rpc: add server/stats/ban/message/whowas/spamfilter providers; fix REHASH to reload raw_config
This commit is contained in:
parent
ee445396eb
commit
89f508aca9
10 changed files with 408 additions and 11 deletions
83
src/modules/rpc/ban.rs
Normal file
83
src/modules/rpc/ban.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
//! rpc ban provider — `xline.list`, `xline.add`, `xline.del`. InspIRCd's
|
||||
//! `m_rpc_ban`. Covers every echoIRCd x-line kind (K/G/Z/E/SHUN/Q/CBAN) through the
|
||||
//! same `add_xline`/`remove_xline` primitives the oper commands use.
|
||||
|
||||
use super::json::{self, obj, qstr};
|
||||
use super::RpcError;
|
||||
use crate::server::Server;
|
||||
use crate::xline::{parse_duration, XKind};
|
||||
|
||||
/// Map a request `type` (letter or unreal-ish name) to an `XKind`.
|
||||
fn kind_of(t: &str) -> Option<XKind> {
|
||||
let up = t.to_ascii_uppercase();
|
||||
XKind::from_tag(&up).or_else(|| {
|
||||
Some(match up.as_str() {
|
||||
"KLINE" => XKind::Kline,
|
||||
"GLINE" => XKind::Gline,
|
||||
"ZLINE" => XKind::Zline,
|
||||
"ELINE" => XKind::Eline,
|
||||
"QLINE" => XKind::Qline,
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcError> {
|
||||
match action {
|
||||
"list" => {
|
||||
let want = json::get_str(params, "type").and_then(|t| kind_of(&t));
|
||||
let items: Vec<String> = s
|
||||
.xlines
|
||||
.iter()
|
||||
.filter(|x| want.is_none_or(|k| x.kind == k))
|
||||
.map(|x| {
|
||||
let expires_at = if x.expires == 0 {
|
||||
"null".to_string()
|
||||
} else {
|
||||
x.expires.to_string()
|
||||
};
|
||||
obj(&[
|
||||
("type", qstr(x.kind.tag())),
|
||||
("mask", qstr(&x.mask)),
|
||||
("reason", qstr(&x.reason)),
|
||||
("setter", qstr(&x.setter)),
|
||||
("expires_at", expires_at),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
Ok(obj(&[("xlines", format!("[{}]", items.join(",")))]))
|
||||
}
|
||||
"add" => {
|
||||
let kind = json::get_str(params, "type")
|
||||
.and_then(|t| kind_of(&t))
|
||||
.ok_or_else(|| RpcError::invalid_params("missing/unknown 'type'"))?;
|
||||
let mask = json::get_str(params, "mask")
|
||||
.or_else(|| json::get_str(params, "name"))
|
||||
.ok_or_else(|| RpcError::invalid_params("missing 'mask'"))?;
|
||||
let duration = json::get_num::<u64>(params, "duration")
|
||||
.or_else(|| {
|
||||
json::get_str(params, "duration_string").and_then(|d| parse_duration(&d))
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let reason = json::get_str(params, "reason").unwrap_or_else(|| "Set via RPC".into());
|
||||
let setter = json::get_str(params, "setter").unwrap_or_else(|| "RPC".into());
|
||||
s.add_xline(kind, &mask, duration, &setter, &reason);
|
||||
Ok(obj(&[("result", "true".into())]))
|
||||
}
|
||||
"del" => {
|
||||
let kind = json::get_str(params, "type")
|
||||
.and_then(|t| kind_of(&t))
|
||||
.ok_or_else(|| RpcError::invalid_params("missing/unknown 'type'"))?;
|
||||
let mask = json::get_str(params, "mask")
|
||||
.or_else(|| json::get_str(params, "name"))
|
||||
.ok_or_else(|| RpcError::invalid_params("missing 'mask'"))?;
|
||||
let removed = s.remove_xline(kind, &mask);
|
||||
if removed {
|
||||
Ok(obj(&[("result", "true".into())]))
|
||||
} else {
|
||||
Err(RpcError::not_found("no matching x-line"))
|
||||
}
|
||||
}
|
||||
other => Err(RpcError::method_not_found(&format!("xline.{other}"))),
|
||||
}
|
||||
}
|
||||
48
src/modules/rpc/message.rs
Normal file
48
src/modules/rpc/message.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//! rpc message provider — `message.send_notice`. InspIRCd's `m_rpc_message`.
|
||||
//! Sends a server NOTICE to a channel (`#…`), a single user (nick), or every
|
||||
//! local user (`*` / `$*`).
|
||||
|
||||
use super::json::{self, obj};
|
||||
use super::RpcError;
|
||||
use crate::server::Server;
|
||||
|
||||
pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcError> {
|
||||
match action {
|
||||
"send_notice" => {
|
||||
let target = json::get_str(params, "target")
|
||||
.ok_or_else(|| RpcError::invalid_params("missing 'target'"))?;
|
||||
let text = json::get_str(params, "message")
|
||||
.ok_or_else(|| RpcError::invalid_params("missing 'message'"))?;
|
||||
let src = s.name.clone();
|
||||
if target == "*" || target == "$*" {
|
||||
let uids: Vec<crate::Uid> = s.users.keys().copied().collect();
|
||||
for uid in uids {
|
||||
let nick = s
|
||||
.users
|
||||
.get(&uid)
|
||||
.map(|u| u.nick.clone())
|
||||
.unwrap_or_default();
|
||||
s.send(uid, format!(":{src} NOTICE {nick} :{text}"));
|
||||
}
|
||||
} else if target.starts_with('#') {
|
||||
let key = target.to_ascii_lowercase();
|
||||
if !s.channels.contains_key(&key) {
|
||||
return Err(RpcError::not_found("no such channel"));
|
||||
}
|
||||
s.to_channel(&key, &format!(":{src} NOTICE {target} :{text}"), None);
|
||||
} else {
|
||||
let uid = s
|
||||
.find_nick(&target)
|
||||
.ok_or_else(|| RpcError::not_found("no such nick"))?;
|
||||
let nick = s
|
||||
.users
|
||||
.get(&uid)
|
||||
.map(|u| u.nick.clone())
|
||||
.unwrap_or_default();
|
||||
s.send(uid, format!(":{src} NOTICE {nick} :{text}"));
|
||||
}
|
||||
Ok(obj(&[("result", "true".into())]))
|
||||
}
|
||||
other => Err(RpcError::method_not_found(&format!("message.{other}"))),
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,17 @@
|
|||
//! (default `127.0.0.1:8080`); every request must carry the token (HTTP Basic or
|
||||
//! Bearer), checked constant-time on the listener thread before anything dispatches.
|
||||
|
||||
pub mod ban;
|
||||
pub mod channel;
|
||||
pub mod core;
|
||||
pub mod httpd;
|
||||
pub mod json;
|
||||
pub mod message;
|
||||
pub mod server;
|
||||
pub mod spamfilter;
|
||||
pub mod stats;
|
||||
pub mod user;
|
||||
pub mod whowas;
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
|
|
@ -77,6 +83,20 @@ pub const ALL_METHODS: &[&str] = &[
|
|||
"channel.get",
|
||||
"channel.kick",
|
||||
"channel.set_topic",
|
||||
"server.list",
|
||||
"server.rehash",
|
||||
"server.disconnect",
|
||||
"module.list",
|
||||
"oper.list",
|
||||
"security_group.list",
|
||||
"xline.list",
|
||||
"xline.add",
|
||||
"xline.del",
|
||||
"message.send_notice",
|
||||
"whowas.get",
|
||||
"spamfilter.list",
|
||||
"spamfilter.add",
|
||||
"spamfilter.del",
|
||||
];
|
||||
|
||||
/// Run a parsed JSON-RPC request on the core thread. `params` is the raw JSON of
|
||||
|
|
@ -86,9 +106,15 @@ pub fn dispatch(s: &mut Server, method: &str, params: &str, id: &str) -> String
|
|||
let result: Result<String, RpcError> = match method {
|
||||
"rpc.methods" | "rpc.info" => core::rpc_info(s, method),
|
||||
"server.info" | "stats.get" => core::server_info(s),
|
||||
"module.list" | "oper.list" | "security_group.list" => stats::handle(s, method, params),
|
||||
_ => match method.split_once('.') {
|
||||
Some(("user", action)) => user::handle(s, action, params),
|
||||
Some(("channel", action)) => channel::handle(s, action, params),
|
||||
Some(("server", action)) => server::handle(s, action, params),
|
||||
Some(("xline", action)) => ban::handle(s, action, params),
|
||||
Some(("message", action)) => message::handle(s, action, params),
|
||||
Some(("whowas", action)) => whowas::handle(s, action, params),
|
||||
Some(("spamfilter", action)) => spamfilter::handle(s, action, params),
|
||||
_ => Err(RpcError::method_not_found(method)),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
62
src/modules/rpc/server.rs
Normal file
62
src/modules/rpc/server.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
//! rpc server provider — `server.list`, `server.rehash`, `server.disconnect`.
|
||||
//! InspIRCd's `m_rpc_server`. (`server.connect` needs the socketengine's dialer,
|
||||
//! which isn't reachable from the core thread — use the `CONNECT` command instead.)
|
||||
|
||||
use super::json::{obj, qstr};
|
||||
use super::RpcError;
|
||||
use crate::config::Config;
|
||||
use crate::server::Server;
|
||||
|
||||
pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcError> {
|
||||
match action {
|
||||
"list" => {
|
||||
let mut servers = vec![obj(&[
|
||||
("name", qstr(&s.name)),
|
||||
("description", qstr(&s.server_desc)),
|
||||
("uplink", qstr("")),
|
||||
("usercount", s.users.len().to_string()),
|
||||
])];
|
||||
for rs in s.servers.values() {
|
||||
let uplink = s
|
||||
.servers
|
||||
.values()
|
||||
.find(|o| o.sid == rs.sid)
|
||||
.map(|_| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
servers.push(obj(&[
|
||||
("name", qstr(&rs.name)),
|
||||
("description", qstr(&rs.desc)),
|
||||
("uplink", qstr(&uplink)),
|
||||
("usercount", "0".to_string()),
|
||||
]));
|
||||
}
|
||||
Ok(obj(&[("servers", format!("[{}]", servers.join(",")))]))
|
||||
}
|
||||
"rehash" => match Config::try_load(&s.conf_path) {
|
||||
Some(fresh) => {
|
||||
s.apply_config(fresh);
|
||||
s.announce("Server configuration reloaded via RPC.");
|
||||
Ok(obj(&[("result", "true".into())]))
|
||||
}
|
||||
None => Err(RpcError::internal("config file could not be read")),
|
||||
},
|
||||
"disconnect" => {
|
||||
let name = json_name(params)?;
|
||||
let via = s
|
||||
.servers
|
||||
.values()
|
||||
.find(|rs| rs.name.eq_ignore_ascii_case(&name))
|
||||
.map(|rs| rs.via)
|
||||
.ok_or_else(|| RpcError::not_found("no such linked server"))?;
|
||||
s.close_link(via, "Disconnected via RPC");
|
||||
Ok(obj(&[("result", "true".into())]))
|
||||
}
|
||||
other => Err(RpcError::method_not_found(&format!("server.{other}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn json_name(params: &str) -> Result<String, RpcError> {
|
||||
super::json::get_str(params, "name")
|
||||
.or_else(|| super::json::get_str(params, "server"))
|
||||
.ok_or_else(|| RpcError::invalid_params("missing 'name'"))
|
||||
}
|
||||
66
src/modules/rpc/spamfilter.rs
Normal file
66
src/modules/rpc/spamfilter.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
//! rpc spamfilter provider — `spamfilter.list`, `spamfilter.add`, `spamfilter.del`.
|
||||
//! InspIRCd's `m_rpc_spamfilter`. Operates on the same [`crate::modules::filter`]
|
||||
//! rule set (stored in `Server.ext`) that the `FILTER` command and the enforcement
|
||||
//! hook use, so a rule added here takes effect immediately.
|
||||
|
||||
use super::json::{self, obj, qstr};
|
||||
use super::RpcError;
|
||||
use crate::modules::filter::{Filters, SpamFilter};
|
||||
use crate::server::Server;
|
||||
|
||||
pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcError> {
|
||||
match action {
|
||||
"list" => {
|
||||
let items: Vec<String> = s
|
||||
.ext
|
||||
.get::<Filters>()
|
||||
.map(|f| {
|
||||
f.0.iter()
|
||||
.map(|r| {
|
||||
obj(&[
|
||||
("pattern", qstr(&r.pattern)),
|
||||
("reason", qstr(&r.reason)),
|
||||
("action", qstr(&r.action)),
|
||||
("duration", r.duration.to_string()),
|
||||
])
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(obj(&[("filters", format!("[{}]", items.join(",")))]))
|
||||
}
|
||||
"add" => {
|
||||
let pattern = json::get_str(params, "pattern")
|
||||
.or_else(|| json::get_str(params, "name"))
|
||||
.ok_or_else(|| RpcError::invalid_params("missing 'pattern'"))?;
|
||||
let action = json::get_str(params, "action").unwrap_or_else(|| "block".into());
|
||||
let reason = json::get_str(params, "reason").unwrap_or_else(|| "Set via RPC".into());
|
||||
let duration = json::get_num::<u64>(params, "duration").unwrap_or(0);
|
||||
let set = s.ext.get_or_insert_with::<Filters>(Filters::default);
|
||||
if set.0.iter().any(|f| f.pattern == pattern) {
|
||||
return Err(RpcError::not_found("filter already exists"));
|
||||
}
|
||||
set.0.push(SpamFilter {
|
||||
pattern,
|
||||
action,
|
||||
duration,
|
||||
reason,
|
||||
});
|
||||
Ok(obj(&[("result", "true".into())]))
|
||||
}
|
||||
"del" => {
|
||||
let pattern = json::get_str(params, "pattern")
|
||||
.or_else(|| json::get_str(params, "name"))
|
||||
.ok_or_else(|| RpcError::invalid_params("missing 'pattern'"))?;
|
||||
let set = s.ext.get_or_insert_with::<Filters>(Filters::default);
|
||||
let before = set.0.len();
|
||||
set.0.retain(|f| f.pattern != pattern);
|
||||
if set.0.len() < before {
|
||||
Ok(obj(&[("result", "true".into())]))
|
||||
} else {
|
||||
Err(RpcError::not_found("no matching filter"))
|
||||
}
|
||||
}
|
||||
other => Err(RpcError::method_not_found(&format!("spamfilter.{other}"))),
|
||||
}
|
||||
}
|
||||
61
src/modules/rpc/stats.rs
Normal file
61
src/modules/rpc/stats.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
//! rpc stats provider — read-only introspection: `module.list`, `oper.list`,
|
||||
//! `security_group.list`. InspIRCd's `m_rpc_stats`.
|
||||
|
||||
use super::json::{obj, qstr};
|
||||
use super::RpcError;
|
||||
use crate::server::Server;
|
||||
|
||||
pub fn handle(s: &mut Server, method: &str, _params: &str) -> Result<String, RpcError> {
|
||||
match method {
|
||||
"module.list" => {
|
||||
let mods: Vec<String> = crate::modules::module_names()
|
||||
.iter()
|
||||
.map(|n| {
|
||||
obj(&[
|
||||
("name", qstr(n)),
|
||||
("description", qstr("")),
|
||||
("version", qstr("")),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
Ok(obj(&[("modules", format!("[{}]", mods.join(",")))]))
|
||||
}
|
||||
"oper.list" => {
|
||||
// configured oper blocks — names only, never the passwords
|
||||
let opers: Vec<String> = s
|
||||
.opers
|
||||
.iter()
|
||||
.map(|(name, _pass)| {
|
||||
obj(&[
|
||||
("name", qstr(name)),
|
||||
("type", qstr("")),
|
||||
("online", "0".into()),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
Ok(obj(&[("opers", format!("[{}]", opers.join(",")))]))
|
||||
}
|
||||
"security_group.list" => {
|
||||
let groups: Vec<String> = s
|
||||
.conf_all("securitygroup")
|
||||
.iter()
|
||||
.filter_map(|line| {
|
||||
let mut it = line.split_whitespace();
|
||||
let name = it.next()?;
|
||||
let criteria = it.collect::<Vec<_>>().join(" ");
|
||||
let public = criteria.split_whitespace().any(|t| t == "public");
|
||||
Some(obj(&[
|
||||
("name", qstr(name)),
|
||||
("criteria", qstr(&criteria)),
|
||||
("public", public.to_string()),
|
||||
]))
|
||||
})
|
||||
.collect();
|
||||
Ok(obj(&[(
|
||||
"security_groups",
|
||||
format!("[{}]", groups.join(",")),
|
||||
)]))
|
||||
}
|
||||
other => Err(RpcError::method_not_found(other)),
|
||||
}
|
||||
}
|
||||
35
src/modules/rpc/whowas.rs
Normal file
35
src/modules/rpc/whowas.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//! rpc whowas provider — `whowas.get`. InspIRCd's `m_rpc_whowas`. Returns the
|
||||
//! recent-nick-history entries the ircd keeps for `WHOWAS`.
|
||||
|
||||
use super::json::{self, obj, qstr};
|
||||
use super::RpcError;
|
||||
use crate::server::Server;
|
||||
|
||||
pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcError> {
|
||||
match action {
|
||||
"get" => {
|
||||
let nick = json::get_str(params, "nick")
|
||||
.ok_or_else(|| RpcError::invalid_params("missing 'nick'"))?;
|
||||
let entries: Vec<String> = s
|
||||
.whowas
|
||||
.iter()
|
||||
.filter(|e| e.nick.eq_ignore_ascii_case(&nick))
|
||||
.map(|e| {
|
||||
let mut fields = vec![
|
||||
("nick", qstr(&e.nick)),
|
||||
("ident", qstr(&e.ident)),
|
||||
("host", qstr(&e.host)),
|
||||
("realname", qstr(&e.realname)),
|
||||
("signon", e.ts.to_string()),
|
||||
];
|
||||
if let Some(acct) = &e.account {
|
||||
fields.push(("account", qstr(acct)));
|
||||
}
|
||||
obj(&fields)
|
||||
})
|
||||
.collect();
|
||||
Ok(obj(&[("entries", format!("[{}]", entries.join(",")))]))
|
||||
}
|
||||
other => Err(RpcError::method_not_found(&format!("whowas.{other}"))),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue