labeled-response + batch: capture a labeled command's replies and wrap them (single label tag / BATCH / ACK)
This commit is contained in:
parent
f6b5e08862
commit
98ac147cc9
5 changed files with 114 additions and 3 deletions
72
src/ircd.rs
72
src/ircd.rs
|
|
@ -46,6 +46,19 @@ pub enum Event {
|
||||||
Tick,
|
Tick,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert an extra IRCv3 tag into a wire line's tag block, creating the `@…`
|
||||||
|
/// block if the line has none. Used to fold `label=`/`batch=` onto captured lines.
|
||||||
|
fn with_extra_tag(line: &str, tag: &str) -> String {
|
||||||
|
if let Some(rest) = line.strip_prefix('@') {
|
||||||
|
match rest.split_once(' ') {
|
||||||
|
Some((tags, body)) => format!("@{tags};{tag} {body}"),
|
||||||
|
None => format!("@{rest};{tag}"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!("@{tag} {line}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Ircd {
|
pub struct Ircd {
|
||||||
server: Server,
|
server: Server,
|
||||||
commands: HashMap<&'static str, Box<dyn Command>>,
|
commands: HashMap<&'static str, Box<dyn Command>>,
|
||||||
|
|
@ -126,7 +139,6 @@ impl Ircd {
|
||||||
u.last_active = crate::server::now();
|
u.last_active = crate::server::now();
|
||||||
u.ping_sent = false;
|
u.ping_sent = false;
|
||||||
}
|
}
|
||||||
let cmd = msg.command.as_str();
|
|
||||||
let registered = self
|
let registered = self
|
||||||
.server
|
.server
|
||||||
.users
|
.users
|
||||||
|
|
@ -140,6 +152,36 @@ impl Ircd {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// labeled-response: if the client tagged this command with `label` and
|
||||||
|
// negotiated the cap, capture its own replies and wrap them with the label.
|
||||||
|
let label = msg.label.clone().filter(|_| {
|
||||||
|
self.server
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| u.caps.labeled_response)
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
if let Some(label) = label {
|
||||||
|
*self.server.label_capture.borrow_mut() = Some((uid, Vec::new()));
|
||||||
|
self.dispatch(uid, &msg, registered);
|
||||||
|
let lines = self
|
||||||
|
.server
|
||||||
|
.label_capture
|
||||||
|
.borrow_mut()
|
||||||
|
.take()
|
||||||
|
.map(|(_, l)| l)
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.emit_labeled(uid, &label, lines);
|
||||||
|
} else {
|
||||||
|
self.dispatch(uid, &msg, registered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run one parsed command: module gates, handler dispatch, post-hooks, and the
|
||||||
|
/// registration/quit follow-ups. Output goes through `Server::send`, so it's
|
||||||
|
/// transparently captured when a labeled command wraps this call.
|
||||||
|
fn dispatch(&mut self, uid: Uid, msg: &message::Message, registered: bool) {
|
||||||
|
let cmd = msg.command.as_str();
|
||||||
// module pre-command gate
|
// module pre-command gate
|
||||||
for m in &mut self.modules {
|
for m in &mut self.modules {
|
||||||
if m.on_pre_command(&mut self.server, uid, cmd, &msg.params) == ModResult::Deny {
|
if m.on_pre_command(&mut self.server, uid, cmd, &msg.params) == ModResult::Deny {
|
||||||
|
|
@ -193,6 +235,34 @@ impl Ircd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit a labeled command's captured replies (labeled-response): `ACK` if it
|
||||||
|
/// produced none, the single line label-tagged if one, else a `BATCH`-wrapped
|
||||||
|
/// group. Runs after the capture is taken, so these go straight to the wire.
|
||||||
|
fn emit_labeled(&mut self, uid: Uid, label: &str, lines: Vec<String>) {
|
||||||
|
let server = self.server.name.clone();
|
||||||
|
match lines.len() {
|
||||||
|
0 => self
|
||||||
|
.server
|
||||||
|
.send(uid, format!("@label={label} :{server} ACK")),
|
||||||
|
1 => {
|
||||||
|
let l = with_extra_tag(&lines[0], &format!("label={label}"));
|
||||||
|
self.server.send(uid, l);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let bref = self.server.next_msgid().replace('-', ""); // batch ref: alnum only
|
||||||
|
self.server.send(
|
||||||
|
uid,
|
||||||
|
format!("@label={label} :{server} BATCH +{bref} labeled-response"),
|
||||||
|
);
|
||||||
|
for l in lines {
|
||||||
|
self.server
|
||||||
|
.send(uid, with_extra_tag(&l, &format!("batch={bref}")));
|
||||||
|
}
|
||||||
|
self.server.send(uid, format!(":{server} BATCH -{bref}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Finish registration if NICK, USER, CAP and the reverse-DNS lookup are all
|
/// Finish registration if NICK, USER, CAP and the reverse-DNS lookup are all
|
||||||
/// done. Called after each command and when a DNS result arrives.
|
/// done. Called after each command and when a DNS result arrives.
|
||||||
fn try_register(&mut self, uid: Uid) {
|
fn try_register(&mut self, uid: Uid) {
|
||||||
|
|
|
||||||
|
|
@ -496,6 +496,7 @@ impl Server {
|
||||||
command: msg.params[1].to_ascii_uppercase(),
|
command: msg.params[1].to_ascii_uppercase(),
|
||||||
params: msg.params[2..].to_vec(),
|
params: msg.params[2..].to_vec(),
|
||||||
ctags: String::new(),
|
ctags: String::new(),
|
||||||
|
label: None,
|
||||||
};
|
};
|
||||||
self.on_link(via, &sub);
|
self.on_link(via, &sub);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ pub struct Message {
|
||||||
pub params: Vec<String>,
|
pub params: Vec<String>,
|
||||||
/// Client-only IRCv3 tags (`+key=val;…`) re-serialised for relay; `""` if none.
|
/// Client-only IRCv3 tags (`+key=val;…`) re-serialised for relay; `""` if none.
|
||||||
pub ctags: String,
|
pub ctags: String,
|
||||||
|
/// The IRCv3 `label` tag value, if the client tagged this command (for
|
||||||
|
/// labeled-response); `None` otherwise.
|
||||||
|
pub label: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse one wire line. Returns `None` for an empty/garbage line.
|
/// Parse one wire line. Returns `None` for an empty/garbage line.
|
||||||
|
|
@ -20,6 +23,7 @@ pub fn parse(line: &str) -> Option<Message> {
|
||||||
|
|
||||||
// IRCv3 message tags — keep the client-only (`+`) tags for relay, drop the rest.
|
// IRCv3 message tags — keep the client-only (`+`) tags for relay, drop the rest.
|
||||||
let mut ctags = String::new();
|
let mut ctags = String::new();
|
||||||
|
let mut label = None;
|
||||||
if let Some(after_at) = rest.strip_prefix('@') {
|
if let Some(after_at) = rest.strip_prefix('@') {
|
||||||
let (tags, r) = after_at.split_once(' ')?;
|
let (tags, r) = after_at.split_once(' ')?;
|
||||||
ctags = tags
|
ctags = tags
|
||||||
|
|
@ -27,6 +31,10 @@ pub fn parse(line: &str) -> Option<Message> {
|
||||||
.filter(|t| t.starts_with('+'))
|
.filter(|t| t.starts_with('+'))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(";");
|
.join(";");
|
||||||
|
label = tags
|
||||||
|
.split(';')
|
||||||
|
.find_map(|t| t.strip_prefix("label="))
|
||||||
|
.map(|v| v.to_string());
|
||||||
rest = r.trim_start();
|
rest = r.trim_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,6 +76,7 @@ pub fn parse(line: &str) -> Option<Message> {
|
||||||
command: cmd.to_ascii_uppercase(),
|
command: cmd.to_ascii_uppercase(),
|
||||||
params,
|
params,
|
||||||
ctags,
|
ctags,
|
||||||
|
label,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
//! keeps usermanager / channelmanager separate from the core. No locks: only the
|
//! keeps usermanager / channelmanager separate from the core. No locks: only the
|
||||||
//! single core thread ever holds a `Server`.
|
//! single core thread ever holds a `Server`.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::net::{SocketAddr, TcpStream};
|
use std::net::{SocketAddr, TcpStream};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
|
@ -105,7 +106,12 @@ pub struct Server {
|
||||||
pub dnsbl_reason: String, // ban reason on a DNSBL hit
|
pub dnsbl_reason: String, // ban reason on a DNSBL hit
|
||||||
pub sasl_server: String, // services server that handles SASL
|
pub sasl_server: String, // services server that handles SASL
|
||||||
pub webirc: Vec<(String, String)>, // trusted web gateways: (password, name)
|
pub webirc: Vec<(String, String)>, // trusted web gateways: (password, name)
|
||||||
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
// labeled-response: while Some((uid, buf)), that client's own responses are
|
||||||
|
// diverted into `buf` instead of the socket, so `on_line` can wrap them with
|
||||||
|
// the command's `label` (single tag, BATCH, or ACK). RefCell because the
|
||||||
|
// output primitives are `&self`.
|
||||||
|
pub label_capture: RefCell<Option<(Uid, Vec<String>)>>,
|
||||||
|
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
|
|
@ -146,6 +152,7 @@ impl Server {
|
||||||
dnsbl_reason: cfg.dnsbl_reason,
|
dnsbl_reason: cfg.dnsbl_reason,
|
||||||
sasl_server: cfg.sasl_server,
|
sasl_server: cfg.sasl_server,
|
||||||
webirc: cfg.webirc,
|
webirc: cfg.webirc,
|
||||||
|
label_capture: RefCell::new(None),
|
||||||
event_tx,
|
event_tx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -399,6 +406,22 @@ impl Server {
|
||||||
} else {
|
} else {
|
||||||
line
|
line
|
||||||
};
|
};
|
||||||
|
self.emit_to(uid, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Final hop for one line to a client: diverted into the labeled-response
|
||||||
|
/// capture buffer when one is active for `uid`, otherwise written to the wire.
|
||||||
|
fn emit_to(&self, uid: Uid, line: String) {
|
||||||
|
if let Ok(mut cap) = self.label_capture.try_borrow_mut() {
|
||||||
|
if let Some((cuid, buf)) = cap.as_mut() {
|
||||||
|
if *cuid == uid {
|
||||||
|
buf.push(line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(u) = self.users.get(&uid) {
|
||||||
u.out.send(line);
|
u.out.send(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -536,7 +559,7 @@ impl Server {
|
||||||
} else {
|
} else {
|
||||||
format!("@{} {body}", tags.join(";"))
|
format!("@{} {body}", tags.join(";"))
|
||||||
};
|
};
|
||||||
u.out.send(line);
|
self.emit_to(uid, line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ pub const SUPPORTED_CAPS: &[&str] = &[
|
||||||
"extended-monitor",
|
"extended-monitor",
|
||||||
"account-tag",
|
"account-tag",
|
||||||
"standard-replies",
|
"standard-replies",
|
||||||
|
"labeled-response",
|
||||||
|
"batch",
|
||||||
"cap-notify",
|
"cap-notify",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -118,6 +120,8 @@ pub struct Caps {
|
||||||
pub extended_monitor: bool, // route away/account/chghost/setname for MONITOR targets
|
pub extended_monitor: bool, // route away/account/chghost/setname for MONITOR targets
|
||||||
pub account_tag: bool, // prepend account=<name> tag on messages from logged-in users
|
pub account_tag: bool, // prepend account=<name> tag on messages from logged-in users
|
||||||
pub standard_replies: bool, // understands FAIL/WARN/NOTE structured replies
|
pub standard_replies: bool, // understands FAIL/WARN/NOTE structured replies
|
||||||
|
pub labeled_response: bool, // tag responses to a labeled command with its label
|
||||||
|
pub batch: bool, // understands BATCH framing
|
||||||
pub cap_notify: bool,
|
pub cap_notify: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,6 +167,8 @@ impl Caps {
|
||||||
"extended-monitor" => self.extended_monitor,
|
"extended-monitor" => self.extended_monitor,
|
||||||
"account-tag" => self.account_tag,
|
"account-tag" => self.account_tag,
|
||||||
"standard-replies" => self.standard_replies,
|
"standard-replies" => self.standard_replies,
|
||||||
|
"labeled-response" => self.labeled_response,
|
||||||
|
"batch" => self.batch,
|
||||||
"cap-notify" => self.cap_notify,
|
"cap-notify" => self.cap_notify,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
|
|
@ -186,6 +192,8 @@ impl Caps {
|
||||||
"extended-monitor" => &mut self.extended_monitor,
|
"extended-monitor" => &mut self.extended_monitor,
|
||||||
"account-tag" => &mut self.account_tag,
|
"account-tag" => &mut self.account_tag,
|
||||||
"standard-replies" => &mut self.standard_replies,
|
"standard-replies" => &mut self.standard_replies,
|
||||||
|
"labeled-response" => &mut self.labeled_response,
|
||||||
|
"batch" => &mut self.batch,
|
||||||
"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