rpc: JSON-RPC-over-HTTP control interface — native httpd + json + dispatch, core provider
This commit is contained in:
parent
9dce3bde92
commit
4b1f096ed8
7 changed files with 617 additions and 0 deletions
119
src/modules/rpc/mod.rs
Normal file
119
src/modules/rpc/mod.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
//! 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<String, RpcError> = 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, RpcError>) -> 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<Event>) {
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue