gardesk/gardm / 202d1bf

Browse files

socket permissions, session and user enumeration

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
202d1bf29240d5341c05f57fa52916238974b240
Parents
b27e1e0
Tree
900c7bf

4 changed files

StatusFile+-
M gardmd/src/ipc.rs 5 0
M gardmd/src/lib.rs 2 0
M gardmd/src/main.rs 4 4
A gardmd/src/sessions.rs 191 0
gardmd/src/ipc.rsmodified
@@ -4,6 +4,7 @@
44
 
55
 use anyhow::Result;
66
 use gardm_ipc::{Request, Response, SOCKET_PATH};
7
+use std::os::unix::fs::PermissionsExt;
78
 use std::path::Path;
89
 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
910
 use tokio::net::{UnixListener, UnixStream};
@@ -35,6 +36,10 @@ impl Server {
3536
         }
3637
 
3738
         let listener = UnixListener::bind(path)?;
39
+
40
+        // Set socket permissions (owner read/write only)
41
+        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
42
+
3843
         tracing::info!("IPC server listening on {}", path.display());
3944
 
4045
         Ok(Self { listener })
gardmd/src/lib.rsmodified
@@ -5,7 +5,9 @@
55
 pub mod auth;
66
 pub mod config;
77
 pub mod ipc;
8
+pub mod sessions;
89
 
910
 pub use auth::{AuthResponse, AuthSession};
1011
 pub use config::Config;
1112
 pub use ipc::{ClientConnection, Server};
13
+pub use sessions::{list_sessions, list_users};
gardmd/src/main.rsmodified
@@ -173,14 +173,14 @@ async fn handle_request(request: Request, auth: &mut AuthSession) -> Response {
173173
 
174174
         Request::ListSessions => {
175175
             tracing::debug!("Listing sessions");
176
-            // TODO: Enumerate /usr/share/xsessions and /usr/share/wayland-sessions
177
-            Response::Sessions { sessions: vec![] }
176
+            let sessions = gardmd::list_sessions();
177
+            Response::Sessions { sessions }
178178
         }
179179
 
180180
         Request::ListUsers => {
181181
             tracing::debug!("Listing users");
182
-            // TODO: Enumerate users from /etc/passwd
183
-            Response::Users { users: vec![] }
182
+            let users = gardmd::list_users();
183
+            Response::Users { users }
184184
         }
185185
     }
186186
 }
gardmd/src/sessions.rsadded
@@ -0,0 +1,191 @@
1
+//! Session and user enumeration
2
+//!
3
+//! Discovers available X11/Wayland sessions and system users.
4
+
5
+use gardm_ipc::{SessionInfo, UserInfo};
6
+use std::fs;
7
+use std::path::{Path, PathBuf};
8
+
9
+/// Directories to search for X11 session .desktop files
10
+const XSESSION_DIRS: &[&str] = &[
11
+    "/usr/share/xsessions",
12
+    "/usr/local/share/xsessions",
13
+];
14
+
15
+/// Directories to search for Wayland session .desktop files
16
+const WAYLAND_SESSION_DIRS: &[&str] = &[
17
+    "/usr/share/wayland-sessions",
18
+    "/usr/local/share/wayland-sessions",
19
+];
20
+
21
+/// Minimum UID for regular users (typically 1000 on most systems)
22
+const MIN_UID: u32 = 1000;
23
+
24
+/// Maximum UID for regular users
25
+const MAX_UID: u32 = 60000;
26
+
27
+/// Enumerate available sessions from .desktop files
28
+pub fn list_sessions() -> Vec<SessionInfo> {
29
+    let mut sessions = Vec::new();
30
+
31
+    // X11 sessions
32
+    for dir in XSESSION_DIRS {
33
+        if let Ok(entries) = fs::read_dir(dir) {
34
+            for entry in entries.flatten() {
35
+                if let Some(session) = parse_desktop_file(&entry.path(), "x11") {
36
+                    sessions.push(session);
37
+                }
38
+            }
39
+        }
40
+    }
41
+
42
+    // Wayland sessions
43
+    for dir in WAYLAND_SESSION_DIRS {
44
+        if let Ok(entries) = fs::read_dir(dir) {
45
+            for entry in entries.flatten() {
46
+                if let Some(session) = parse_desktop_file(&entry.path(), "wayland") {
47
+                    sessions.push(session);
48
+                }
49
+            }
50
+        }
51
+    }
52
+
53
+    // Sort by name for consistent ordering
54
+    sessions.sort_by(|a, b| a.name.cmp(&b.name));
55
+
56
+    sessions
57
+}
58
+
59
+/// Parse a .desktop file into SessionInfo
60
+fn parse_desktop_file(path: &Path, session_type: &str) -> Option<SessionInfo> {
61
+    let filename = path.file_name()?.to_str()?;
62
+    if !filename.ends_with(".desktop") {
63
+        return None;
64
+    }
65
+
66
+    let content = fs::read_to_string(path).ok()?;
67
+    let mut name = None;
68
+    let mut comment = None;
69
+    let mut exec = None;
70
+
71
+    for line in content.lines() {
72
+        let line = line.trim();
73
+        if line.starts_with("Name=") {
74
+            name = Some(line.strip_prefix("Name=")?.to_string());
75
+        } else if line.starts_with("Comment=") {
76
+            comment = Some(line.strip_prefix("Comment=")?.to_string());
77
+        } else if line.starts_with("Exec=") {
78
+            exec = Some(line.strip_prefix("Exec=")?.to_string());
79
+        }
80
+    }
81
+
82
+    let id = filename.strip_suffix(".desktop")?.to_string();
83
+    let name = name.unwrap_or_else(|| id.clone());
84
+    let exec = exec?;
85
+
86
+    Some(SessionInfo {
87
+        id,
88
+        name,
89
+        comment,
90
+        exec,
91
+        session_type: session_type.to_string(),
92
+    })
93
+}
94
+
95
+/// Enumerate users suitable for login
96
+pub fn list_users() -> Vec<UserInfo> {
97
+    let mut users = Vec::new();
98
+
99
+    // Read /etc/passwd
100
+    if let Ok(content) = fs::read_to_string("/etc/passwd") {
101
+        for line in content.lines() {
102
+            if let Some(user) = parse_passwd_line(line) {
103
+                users.push(user);
104
+            }
105
+        }
106
+    }
107
+
108
+    // Sort by username
109
+    users.sort_by(|a, b| a.name.cmp(&b.name));
110
+
111
+    users
112
+}
113
+
114
+/// Parse a line from /etc/passwd
115
+fn parse_passwd_line(line: &str) -> Option<UserInfo> {
116
+    let fields: Vec<&str> = line.split(':').collect();
117
+    if fields.len() < 7 {
118
+        return None;
119
+    }
120
+
121
+    let name = fields[0].to_string();
122
+    let uid: u32 = fields[2].parse().ok()?;
123
+    let gecos = fields[4];
124
+    let home = PathBuf::from(fields[5]);
125
+    let shell = fields[6];
126
+
127
+    // Filter: only regular users (UID in range, valid shell)
128
+    if uid < MIN_UID || uid > MAX_UID {
129
+        return None;
130
+    }
131
+
132
+    // Skip users with nologin shell
133
+    if shell.contains("nologin") || shell.contains("false") {
134
+        return None;
135
+    }
136
+
137
+    // Parse GECOS field (full name is first comma-separated field)
138
+    let full_name = if gecos.is_empty() {
139
+        None
140
+    } else {
141
+        Some(gecos.split(',').next()?.to_string())
142
+    };
143
+
144
+    // Check for user avatar
145
+    let avatar = find_user_avatar(&name, &home);
146
+
147
+    Some(UserInfo {
148
+        name,
149
+        full_name,
150
+        home,
151
+        avatar,
152
+    })
153
+}
154
+
155
+/// Find user avatar image if available
156
+fn find_user_avatar(username: &str, home: &Path) -> Option<PathBuf> {
157
+    // Check common avatar locations
158
+    let candidates = [
159
+        home.join(".face"),
160
+        home.join(".face.icon"),
161
+        PathBuf::from(format!("/var/lib/AccountsService/icons/{}", username)),
162
+    ];
163
+
164
+    for path in &candidates {
165
+        if path.exists() {
166
+            return Some(path.clone());
167
+        }
168
+    }
169
+
170
+    None
171
+}
172
+
173
+#[cfg(test)]
174
+mod tests {
175
+    use super::*;
176
+
177
+    #[test]
178
+    fn test_list_sessions() {
179
+        // This test may return empty on systems without xsessions
180
+        let sessions = list_sessions();
181
+        // Just verify it doesn't panic
182
+        println!("Found {} sessions", sessions.len());
183
+    }
184
+
185
+    #[test]
186
+    fn test_list_users() {
187
+        let users = list_users();
188
+        // Should find at least the current user on most systems
189
+        println!("Found {} users", users.len());
190
+    }
191
+}