use std::path::Path;
use serde::{Deserialize, Serialize};
pub use secureops_core::{AuditOptions, AuditReport, MonitorAlert, MonitorStatus};
#[derive(Debug, thiserror::Error)]
pub enum IpcError {
#[error("ipc transport i/o error: {0}")]
Io(#[from] std::io::Error),
#[error("ipc codec error: {0}")]
Codec(#[from] serde_json::Error),
#[error("ipc peer authentication denied: {0}")]
Unauthorized(String),
#[error("ipc peer-cred not supported on this platform")]
UnsupportedPlatform,
}
pub type IpcResult<T> = std::result::Result<T, IpcError>;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum IpcRequest {
Audit {
#[serde(default)]
deep: bool,
#[serde(default)]
fix: bool,
#[serde(default)]
json: bool,
},
Status,
Kill {
reason: Option<String>,
},
Subscribe,
Alerts {
limit: Option<u32>,
},
ReloadPolicy,
Ping,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum IpcResponse {
Ok(serde_json::Value),
Err(String),
Alert(MonitorAlert),
}
impl IpcResponse {
pub fn ok<T: Serialize>(value: &T) -> IpcResult<Self> {
Ok(IpcResponse::Ok(serde_json::to_value(value)?))
}
pub fn err(msg: impl std::fmt::Display) -> Self {
IpcResponse::Err(msg.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerCred {
pub uid: u32,
pub pid: i32,
}
impl PeerCred {
pub fn is_authorized(&self, allowed_uid: u32) -> bool {
self.uid == allowed_uid
}
}
#[cfg(unix)]
pub fn peer_cred(stream: &tokio::net::UnixStream) -> std::io::Result<PeerCred> {
let ucred = stream.peer_cred()?;
Ok(PeerCred {
uid: ucred.uid(),
pid: ucred.pid().unwrap_or(-1),
})
}
#[async_trait::async_trait]
pub trait IpcHandler: Send + Sync {
async fn handle(&self, peer: PeerCred, request: IpcRequest) -> IpcResponse;
}
#[cfg(unix)]
pub async fn serve<H, P>(path: P, handler: H) -> IpcResult<()>
where
H: IpcHandler + 'static,
P: AsRef<Path>,
{
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixListener;
let listener = UnixListener::bind(path.as_ref())?;
let handler = Arc::new(handler);
loop {
let (stream, _) = listener.accept().await?;
let peer = peer_cred(&stream).unwrap_or(PeerCred {
uid: u32::MAX,
pid: -1,
});
let handler = handler.clone();
tokio::spawn(async move {
let (read_half, mut write_half) = stream.into_split();
let mut lines = BufReader::new(read_half).lines();
while let Ok(Some(line)) = lines.next_line().await {
let request: IpcRequest = match serde_json::from_str(&line) {
Ok(r) => r,
Err(e) => {
let resp = IpcResponse::err(e);
let _ = write_half
.write_all(
format!("{}\n", serde_json::to_string(&resp).unwrap_or_default())
.as_bytes(),
)
.await;
continue;
}
};
let response = handler.handle(peer, request).await;
let _ = write_half
.write_all(
format!("{}\n", serde_json::to_string(&response).unwrap_or_default())
.as_bytes(),
)
.await;
}
});
}
}
#[cfg(unix)]
pub struct IpcClient {
stream: tokio::net::UnixStream,
}
#[cfg(unix)]
impl IpcClient {
pub async fn request(&mut self, request: IpcRequest) -> IpcResult<IpcResponse> {
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
let json = serde_json::to_string(&request)?;
let (read_half, mut write_half) = self.stream.split();
write_half
.write_all(format!("{}\n", json).as_bytes())
.await?;
let mut reader = BufReader::new(read_half);
let mut line = String::new();
reader.read_line(&mut line).await?;
let response: IpcResponse = serde_json::from_str(line.trim())?;
Ok(response)
}
}
#[cfg(unix)]
pub async fn connect<P: AsRef<Path>>(path: P) -> IpcResult<IpcClient> {
use tokio::net::UnixStream;
let stream = UnixStream::connect(path.as_ref()).await?;
Ok(IpcClient { stream })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips_through_json() {
let req = IpcRequest::Kill {
reason: Some("manual trip".to_string()),
};
let bytes = serde_json::to_vec(&req).expect("serialize");
let back: IpcRequest = serde_json::from_slice(&bytes).expect("deserialize");
assert_eq!(req, back);
}
#[test]
fn response_ok_wraps_value() {
let resp = IpcResponse::ok(&"pong").expect("ok wrap");
match resp {
IpcResponse::Ok(v) => assert_eq!(v, serde_json::json!("pong")),
other => panic!("expected Ok, got {other:?}"),
}
}
#[test]
fn peer_cred_authorization() {
let pc = PeerCred {
uid: 501,
pid: 4242,
};
assert!(pc.is_authorized(501));
assert!(!pc.is_authorized(0));
}
}