nevaforget 226bbb75e4 Rewrite moongreet from Python to Rust (v0.3.0)
Complete rewrite of the greetd greeter from Python/PyGObject to Rust/gtk4-rs
for consistency with moonset, single binary without Python runtime, and
improved security through Rust memory safety.

Modules: main, greeter, ipc, config, i18n, users, sessions, power
86 unit tests covering all modules including login_worker IPC flow.
Security hardening: shell-word splitting for exec_cmd, absolute path
validation for session binaries, session-name sanitization, absolute
loginctl path, atomic IPC writes.
2026-03-27 22:08:33 +01:00

295 lines
8.9 KiB
Rust

// ABOUTME: greetd IPC protocol implementation — communicates via Unix socket.
// ABOUTME: Uses length-prefixed JSON encoding as specified by the greetd IPC protocol.
use std::io::{self, Read, Write};
use std::os::unix::net::UnixStream;
const MAX_PAYLOAD_SIZE: usize = 65536;
/// Errors from greetd IPC communication.
#[derive(Debug)]
pub enum IpcError {
Io(io::Error),
PayloadTooLarge(usize),
Json(serde_json::Error),
ConnectionClosed,
}
impl std::fmt::Display for IpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IpcError::Io(e) => write!(f, "IPC I/O error: {e}"),
IpcError::PayloadTooLarge(size) => {
write!(f, "Payload too large: {size} bytes (max {MAX_PAYLOAD_SIZE})")
}
IpcError::Json(e) => write!(f, "IPC JSON error: {e}"),
IpcError::ConnectionClosed => write!(f, "Connection closed while reading data"),
}
}
}
impl std::error::Error for IpcError {}
impl From<io::Error> for IpcError {
fn from(e: io::Error) -> Self {
IpcError::Io(e)
}
}
impl From<serde_json::Error> for IpcError {
fn from(e: serde_json::Error) -> Self {
IpcError::Json(e)
}
}
/// Read exactly 4 bytes (length header) from the stream into a stack array.
fn recv_header(stream: &mut UnixStream) -> Result<[u8; 4], IpcError> {
let mut buf = [0u8; 4];
let mut filled = 0;
while filled < 4 {
let bytes_read = stream.read(&mut buf[filled..])?;
if bytes_read == 0 {
return Err(IpcError::ConnectionClosed);
}
filled += bytes_read;
}
Ok(buf)
}
/// Receive exactly n bytes from the stream, looping on partial reads.
fn recv_payload(stream: &mut UnixStream, n: usize) -> Result<Vec<u8>, IpcError> {
let mut buf = vec![0u8; n];
let mut filled = 0;
while filled < n {
let bytes_read = stream.read(&mut buf[filled..])?;
if bytes_read == 0 {
return Err(IpcError::ConnectionClosed);
}
filled += bytes_read;
}
Ok(buf)
}
/// Send a length-prefixed JSON message to the greetd socket.
/// Header and payload are sent in a single write for atomicity.
pub fn send_message(
stream: &mut UnixStream,
msg: &serde_json::Value,
) -> Result<(), IpcError> {
let payload = serde_json::to_vec(msg)?;
if payload.len() > MAX_PAYLOAD_SIZE {
return Err(IpcError::PayloadTooLarge(payload.len()));
}
let header = (payload.len() as u32).to_le_bytes();
let mut buf = Vec::with_capacity(4 + payload.len());
buf.extend_from_slice(&header);
buf.extend_from_slice(&payload);
stream.write_all(&buf)?;
Ok(())
}
/// Receive a length-prefixed JSON message from the greetd socket.
pub fn recv_message(stream: &mut UnixStream) -> Result<serde_json::Value, IpcError> {
let header = recv_header(stream)?;
let length = u32::from_le_bytes(header) as usize;
if length > MAX_PAYLOAD_SIZE {
return Err(IpcError::PayloadTooLarge(length));
}
let payload = recv_payload(stream, length)?;
let value: serde_json::Value = serde_json::from_slice(&payload)?;
Ok(value)
}
/// Send a create_session request to greetd and return the response.
pub fn create_session(
stream: &mut UnixStream,
username: &str,
) -> Result<serde_json::Value, IpcError> {
let msg = serde_json::json!({
"type": "create_session",
"username": username,
});
send_message(stream, &msg)?;
recv_message(stream)
}
/// Send an authentication response (e.g. password) to greetd.
pub fn post_auth_response(
stream: &mut UnixStream,
response: Option<&str>,
) -> Result<serde_json::Value, IpcError> {
let msg = serde_json::json!({
"type": "post_auth_message_response",
"response": response,
});
send_message(stream, &msg)?;
recv_message(stream)
}
/// Send a start_session request to launch the user's session.
pub fn start_session(
stream: &mut UnixStream,
cmd: &[String],
) -> Result<serde_json::Value, IpcError> {
let msg = serde_json::json!({
"type": "start_session",
"cmd": cmd,
});
send_message(stream, &msg)?;
recv_message(stream)
}
/// Cancel the current authentication session.
pub fn cancel_session(stream: &mut UnixStream) -> Result<serde_json::Value, IpcError> {
let msg = serde_json::json!({"type": "cancel_session"});
send_message(stream, &msg)?;
recv_message(stream)
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::net::UnixStream;
/// Create a connected pair of Unix sockets for testing.
fn socket_pair() -> (UnixStream, UnixStream) {
UnixStream::pair().unwrap()
}
#[test]
fn send_and_receive_message() {
let (mut client, mut server) = socket_pair();
let msg = serde_json::json!({"type": "create_session", "username": "test"});
send_message(&mut client, &msg).unwrap();
let received = recv_message(&mut server).unwrap();
assert_eq!(received["type"], "create_session");
assert_eq!(received["username"], "test");
}
#[test]
fn create_session_roundtrip() {
let (mut client, mut server) = socket_pair();
// Simulate greetd response in a thread
let handle = std::thread::spawn(move || {
let msg = recv_message(&mut server).unwrap();
assert_eq!(msg["type"], "create_session");
assert_eq!(msg["username"], "alice");
let response = serde_json::json!({
"type": "auth_message",
"auth_message_type": "visible",
"auth_message": "Password: ",
});
send_message(&mut server, &response).unwrap();
});
let response = create_session(&mut client, "alice").unwrap();
assert_eq!(response["type"], "auth_message");
handle.join().unwrap();
}
#[test]
fn post_auth_response_roundtrip() {
let (mut client, mut server) = socket_pair();
let handle = std::thread::spawn(move || {
let msg = recv_message(&mut server).unwrap();
assert_eq!(msg["type"], "post_auth_message_response");
assert_eq!(msg["response"], "secret123");
let response = serde_json::json!({"type": "success"});
send_message(&mut server, &response).unwrap();
});
let response = post_auth_response(&mut client, Some("secret123")).unwrap();
assert_eq!(response["type"], "success");
handle.join().unwrap();
}
#[test]
fn start_session_roundtrip() {
let (mut client, mut server) = socket_pair();
let handle = std::thread::spawn(move || {
let msg = recv_message(&mut server).unwrap();
assert_eq!(msg["type"], "start_session");
assert_eq!(msg["cmd"], serde_json::json!(["niri-session"]));
let response = serde_json::json!({"type": "success"});
send_message(&mut server, &response).unwrap();
});
let cmd = vec!["niri-session".to_string()];
let response = start_session(&mut client, &cmd).unwrap();
assert_eq!(response["type"], "success");
handle.join().unwrap();
}
#[test]
fn cancel_session_roundtrip() {
let (mut client, mut server) = socket_pair();
let handle = std::thread::spawn(move || {
let msg = recv_message(&mut server).unwrap();
assert_eq!(msg["type"], "cancel_session");
let response = serde_json::json!({"type": "success"});
send_message(&mut server, &response).unwrap();
});
let response = cancel_session(&mut client).unwrap();
assert_eq!(response["type"], "success");
handle.join().unwrap();
}
#[test]
fn connection_closed_returns_error() {
let (mut client, server) = socket_pair();
drop(server);
let result = recv_message(&mut client);
assert!(result.is_err());
}
#[test]
fn oversized_payload_rejected_on_send() {
let (mut client, _server) = socket_pair();
let big_string = "x".repeat(MAX_PAYLOAD_SIZE + 1);
let msg = serde_json::json!({"data": big_string});
let result = send_message(&mut client, &msg);
assert!(result.is_err());
}
#[test]
fn oversized_payload_rejected_on_receive() {
let (mut client, mut server) = socket_pair();
// Manually send a header claiming a huge payload
let fake_length: u32 = (MAX_PAYLOAD_SIZE as u32) + 1;
server.write_all(&fake_length.to_le_bytes()).unwrap();
let result = recv_message(&mut client);
assert!(matches!(result, Err(IpcError::PayloadTooLarge(_))));
}
#[test]
fn ipc_error_display() {
let err = IpcError::ConnectionClosed;
assert_eq!(err.to_string(), "Connection closed while reading data");
let err = IpcError::PayloadTooLarge(99999);
assert!(err.to_string().contains("99999"));
}
}