| 1 | //! IPC protocol for gardm display manager |
| 2 | //! |
| 3 | //! Defines the JSON-based protocol between gardmd (daemon) and gardm-greeter (UI). |
| 4 | |
| 5 | use serde::{Deserialize, Serialize}; |
| 6 | use std::path::PathBuf; |
| 7 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; |
| 8 | use tokio::net::UnixStream; |
| 9 | |
| 10 | /// Socket path for gardm IPC |
| 11 | pub const SOCKET_PATH: &str = "/run/gardm.sock"; |
| 12 | |
| 13 | /// Default session type for backward compatibility |
| 14 | fn default_session_type() -> String { |
| 15 | "x11".to_string() |
| 16 | } |
| 17 | |
| 18 | /// Requests from greeter to daemon |
| 19 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 20 | #[serde(tag = "type", rename_all = "snake_case")] |
| 21 | pub enum Request { |
| 22 | /// Create a new authentication session for a user |
| 23 | CreateSession { username: String }, |
| 24 | |
| 25 | /// Provide authentication response (password, OTP, etc.) |
| 26 | Authenticate { response: String }, |
| 27 | |
| 28 | /// Start the user's session after successful auth |
| 29 | StartSession { |
| 30 | /// Session command (e.g., ["gar-session.sh"]) |
| 31 | cmd: Vec<String>, |
| 32 | /// Session type: "x11" or "wayland" |
| 33 | #[serde(default = "default_session_type")] |
| 34 | session_type: String, |
| 35 | /// Additional environment variables |
| 36 | #[serde(default)] |
| 37 | env: Vec<String>, |
| 38 | }, |
| 39 | |
| 40 | /// Cancel the current authentication attempt |
| 41 | CancelSession, |
| 42 | |
| 43 | /// Request system shutdown |
| 44 | Shutdown, |
| 45 | |
| 46 | /// Request system reboot |
| 47 | Reboot, |
| 48 | |
| 49 | /// Request system suspend |
| 50 | Suspend, |
| 51 | |
| 52 | /// Get list of available sessions |
| 53 | ListSessions, |
| 54 | |
| 55 | /// Get list of available users |
| 56 | ListUsers, |
| 57 | } |
| 58 | |
| 59 | /// Responses from daemon to greeter |
| 60 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 61 | #[serde(tag = "type", rename_all = "snake_case")] |
| 62 | pub enum Response { |
| 63 | /// Operation completed successfully |
| 64 | Success, |
| 65 | |
| 66 | /// PAM is requesting user input |
| 67 | AuthPrompt { |
| 68 | /// Prompt message (e.g., "Password:") |
| 69 | prompt: String, |
| 70 | /// Whether to echo input (false for passwords) |
| 71 | echo: bool, |
| 72 | }, |
| 73 | |
| 74 | /// PAM informational message |
| 75 | AuthInfo { message: String }, |
| 76 | |
| 77 | /// Authentication failed |
| 78 | AuthError { message: String }, |
| 79 | |
| 80 | /// General error |
| 81 | Error { message: String }, |
| 82 | |
| 83 | /// List of available sessions |
| 84 | Sessions { |
| 85 | sessions: Vec<SessionInfo>, |
| 86 | /// Default session ID from daemon config |
| 87 | #[serde(default)] |
| 88 | default_session: Option<String>, |
| 89 | }, |
| 90 | |
| 91 | /// List of available users |
| 92 | Users { users: Vec<UserInfo> }, |
| 93 | } |
| 94 | |
| 95 | /// Information about an available session |
| 96 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 97 | pub struct SessionInfo { |
| 98 | /// Session identifier (desktop file name without .desktop) |
| 99 | pub id: String, |
| 100 | /// Display name |
| 101 | pub name: String, |
| 102 | /// Optional comment/description |
| 103 | pub comment: Option<String>, |
| 104 | /// Exec command |
| 105 | pub exec: String, |
| 106 | /// Session type (x11, wayland) |
| 107 | pub session_type: String, |
| 108 | } |
| 109 | |
| 110 | /// Information about a user |
| 111 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 112 | pub struct UserInfo { |
| 113 | /// Username |
| 114 | pub name: String, |
| 115 | /// Full name (GECOS field) |
| 116 | pub full_name: Option<String>, |
| 117 | /// Home directory |
| 118 | pub home: PathBuf, |
| 119 | /// Path to user avatar (if available) |
| 120 | pub avatar: Option<PathBuf>, |
| 121 | } |
| 122 | |
| 123 | /// IPC client for connecting to gardmd |
| 124 | pub struct Client { |
| 125 | reader: BufReader<tokio::net::unix::OwnedReadHalf>, |
| 126 | writer: tokio::net::unix::OwnedWriteHalf, |
| 127 | } |
| 128 | |
| 129 | impl Client { |
| 130 | /// Connect to the daemon |
| 131 | pub async fn connect() -> Result<Self, std::io::Error> { |
| 132 | Self::connect_to(SOCKET_PATH).await |
| 133 | } |
| 134 | |
| 135 | /// Connect to a specific socket path |
| 136 | pub async fn connect_to(path: &str) -> Result<Self, std::io::Error> { |
| 137 | let stream = UnixStream::connect(path).await?; |
| 138 | let (read, write) = stream.into_split(); |
| 139 | Ok(Self { |
| 140 | reader: BufReader::new(read), |
| 141 | writer: write, |
| 142 | }) |
| 143 | } |
| 144 | |
| 145 | /// Send a request to the daemon |
| 146 | pub async fn send(&mut self, request: &Request) -> Result<(), std::io::Error> { |
| 147 | let json = serde_json::to_string(request).map_err(|e| { |
| 148 | std::io::Error::new(std::io::ErrorKind::InvalidData, e) |
| 149 | })?; |
| 150 | self.writer.write_all(json.as_bytes()).await?; |
| 151 | self.writer.write_all(b"\n").await?; |
| 152 | self.writer.flush().await?; |
| 153 | Ok(()) |
| 154 | } |
| 155 | |
| 156 | /// Receive a response from the daemon |
| 157 | pub async fn recv(&mut self) -> Result<Response, std::io::Error> { |
| 158 | let mut line = String::new(); |
| 159 | let n = self.reader.read_line(&mut line).await?; |
| 160 | if n == 0 { |
| 161 | return Err(std::io::Error::new( |
| 162 | std::io::ErrorKind::UnexpectedEof, |
| 163 | "daemon closed connection", |
| 164 | )); |
| 165 | } |
| 166 | serde_json::from_str(&line).map_err(|e| { |
| 167 | std::io::Error::new(std::io::ErrorKind::InvalidData, e) |
| 168 | }) |
| 169 | } |
| 170 | |
| 171 | /// Send request and wait for response |
| 172 | pub async fn request(&mut self, request: &Request) -> Result<Response, std::io::Error> { |
| 173 | self.send(request).await?; |
| 174 | self.recv().await |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | #[cfg(test)] |
| 179 | mod tests { |
| 180 | use super::*; |
| 181 | |
| 182 | #[test] |
| 183 | fn test_request_serialization() { |
| 184 | let req = Request::CreateSession { |
| 185 | username: "testuser".to_string(), |
| 186 | }; |
| 187 | let json = serde_json::to_string(&req).unwrap(); |
| 188 | assert!(json.contains("create_session")); |
| 189 | assert!(json.contains("testuser")); |
| 190 | } |
| 191 | |
| 192 | #[test] |
| 193 | fn test_response_serialization() { |
| 194 | let resp = Response::AuthPrompt { |
| 195 | prompt: "Password:".to_string(), |
| 196 | echo: false, |
| 197 | }; |
| 198 | let json = serde_json::to_string(&resp).unwrap(); |
| 199 | assert!(json.contains("auth_prompt")); |
| 200 | assert!(json.contains("Password:")); |
| 201 | } |
| 202 | |
| 203 | #[test] |
| 204 | fn test_request_deserialization() { |
| 205 | let json = r#"{"type":"authenticate","response":"secret123"}"#; |
| 206 | let req: Request = serde_json::from_str(json).unwrap(); |
| 207 | match req { |
| 208 | Request::Authenticate { response } => assert_eq!(response, "secret123"), |
| 209 | _ => panic!("Wrong variant"), |
| 210 | } |
| 211 | } |
| 212 | } |
| 213 |