//! rpc — a JSON-RPC 2.0 control interface over a small native HTTP server, the //! echoIRCd analogue of InspIRCd's `m_httpd` + `m_jsonrpc` + `m_rpc_*`. Admin tools //! call it to introspect and drive the ircd (list/kill users, manage bans, rehash…). //! //! Layering (each provider is its own file, per [[echoircd-module-per-file]]): //! * [`httpd`] — the listener thread: accept, parse HTTP, authenticate, and hand //! the JSON-RPC body to the core as `Event::RpcRequest`. //! * [`json`] — native JSON scan/build (no serde). //! * `core` / `user` / `channel` / `server` / `stats` / `ban` / `message` / //! `whowas` / `spamfilter` / `log` — the method providers, called on the core //! thread with `&mut Server`. //! //! Security: **off** unless `rpc = yes` AND `rpc_token` is set; binds `rpc_bind` //! (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 core; pub mod httpd; pub mod json; use std::sync::mpsc::Sender; use crate::config::Config; use crate::ircd::Event; use crate::server::Server; /// A JSON-RPC error (standard codes plus app-specific negatives). pub struct RpcError { pub code: i64, pub message: String, } impl RpcError { pub fn method_not_found(m: &str) -> RpcError { RpcError { code: -32601, message: format!("Method not found: {m}"), } } pub fn invalid_params(msg: &str) -> RpcError { RpcError { code: -32602, message: msg.to_string(), } } pub fn not_found(msg: &str) -> RpcError { RpcError { code: -1000, message: msg.to_string(), } } pub fn internal(msg: &str) -> RpcError { RpcError { code: -32603, message: msg.to_string(), } } } /// Every method name the interface exposes (drives `rpc.methods`). Keep in sync /// with the `dispatch` routes as providers are added. pub const ALL_METHODS: &[&str] = &["rpc.methods", "rpc.info", "server.info", "stats.get"]; /// Run a parsed JSON-RPC request on the core thread. `params` is the raw JSON of /// the `params` member (`{}` if none); `id` is the raw JSON of the request id /// (echoed verbatim). Returns the full JSON-RPC response envelope. pub fn dispatch(s: &mut Server, method: &str, _params: &str, id: &str) -> String { // `_params` is consumed once the param-taking providers (user/channel/…) land. let result: Result = match method { "rpc.methods" | "rpc.info" => core::rpc_info(s, method), "server.info" | "stats.get" => core::server_info(s), other => Err(RpcError::method_not_found(other)), }; envelope(method, id, result) } /// Wrap a provider result (or error) in the JSON-RPC 2.0 response envelope, echoing /// the method and id (InspIRCd includes the method in its responses too). pub fn envelope(method: &str, id: &str, result: Result) -> String { let id = if id.trim().is_empty() { "null" } else { id }; match result { Ok(res) => format!( "{{\"jsonrpc\":\"2.0\",\"method\":{},\"result\":{res},\"id\":{id}}}", json::qstr(method) ), Err(e) => format!( "{{\"jsonrpc\":\"2.0\",\"method\":{},\"error\":{{\"code\":{},\"message\":{}}},\"id\":{id}}}", json::qstr(method), e.code, json::qstr(&e.message) ), } } /// Start the RPC HTTP listener if configured. Called from `main` with a clone of /// the core's event sender. No-op (with a stderr note) when disabled or misconfigured. pub fn maybe_start(cfg: &Config, tx: Sender) { let get = |k: &str| cfg.raw.get(k).and_then(|v| v.last()).map(|s| s.as_str()); let on = get("rpc").map(crate::config::yesish).unwrap_or(false); if !on { return; } let token = match get("rpc_token") { Some(t) if !t.is_empty() => t.to_string(), _ => { eprintln!("echoircd: rpc enabled but no rpc_token set — RPC stays OFF"); return; } }; let bind = get("rpc_bind").unwrap_or("127.0.0.1:8080").to_string(); let user = get("rpc_user").unwrap_or("admin").to_string(); match std::net::TcpListener::bind(&bind) { Ok(listener) => { eprintln!("echoircd JSON-RPC on {bind} (token auth)"); std::thread::spawn(move || httpd::serve(listener, tx, user, token)); } Err(e) => eprintln!("echoircd: cannot bind rpc {bind}: {e}"), } }