gardesk/garlock / 4e4bf68

Browse files

Add X11 window with keyboard/pointer grabs

- LockerWindow for fullscreen override-redirect window
- Keyboard and pointer grabs for security
- RandR monitor detection for multi-monitor support
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
4e4bf680c8cef2e1a64645e076548503a6622f54
Parents
7e11bac
Tree
6081ffc

3 changed files

StatusFile+-
A garlock/src/x11/mod.rs 9 0
A garlock/src/x11/monitors.rs 180 0
A garlock/src/x11/window.rs 295 0
garlock/src/x11/mod.rsadded
@@ -0,0 +1,9 @@
1
+//! X11 window management for garlock
2
+//!
3
+//! Provides fullscreen window creation with secure keyboard/pointer grabs.
4
+
5
+mod window;
6
+mod monitors;
7
+
8
+pub use window::LockerWindow;
9
+pub use monitors::{Monitor, MonitorConfig};
garlock/src/x11/monitors.rsadded
@@ -0,0 +1,180 @@
1
+//! Monitor detection using RandR extension
2
+//!
3
+//! Detects connected monitors and their positions for proper
4
+//! multi-monitor support.
5
+
6
+use anyhow::{Context, Result};
7
+use x11rb::protocol::randr::{self, ConnectionExt as RandrConnectionExt};
8
+use x11rb::protocol::xproto::Window;
9
+use x11rb::rust_connection::RustConnection;
10
+
11
+/// Information about a single monitor
12
+#[derive(Debug, Clone)]
13
+pub struct Monitor {
14
+    /// Monitor name (e.g., "eDP-1", "HDMI-1")
15
+    pub name: String,
16
+    /// X position in virtual screen
17
+    pub x: i16,
18
+    /// Y position in virtual screen
19
+    pub y: i16,
20
+    /// Width in pixels
21
+    pub width: u16,
22
+    /// Height in pixels
23
+    pub height: u16,
24
+    /// Whether this is the primary monitor
25
+    pub primary: bool,
26
+}
27
+
28
+impl Monitor {
29
+    /// Get the center point of this monitor
30
+    pub fn center(&self) -> (f64, f64) {
31
+        let cx = self.x as f64 + self.width as f64 / 2.0;
32
+        let cy = self.y as f64 + self.height as f64 / 2.0;
33
+        (cx, cy)
34
+    }
35
+}
36
+
37
+/// Configuration of all connected monitors
38
+#[derive(Debug, Clone)]
39
+pub struct MonitorConfig {
40
+    /// List of connected monitors
41
+    pub monitors: Vec<Monitor>,
42
+    /// Total virtual screen width
43
+    pub total_width: u16,
44
+    /// Total virtual screen height
45
+    pub total_height: u16,
46
+}
47
+
48
+impl MonitorConfig {
49
+    /// Detect monitors using RandR extension
50
+    pub fn detect(conn: &RustConnection, root: Window) -> Result<Self> {
51
+        // Query RandR version to ensure it's available
52
+        let version = conn
53
+            .randr_query_version(1, 5)?
54
+            .reply()
55
+            .context("RandR not available")?;
56
+
57
+        tracing::debug!(
58
+            major = version.major_version,
59
+            minor = version.minor_version,
60
+            "RandR version"
61
+        );
62
+
63
+        // Get screen resources
64
+        let resources = conn
65
+            .randr_get_screen_resources_current(root)?
66
+            .reply()
67
+            .context("Failed to get screen resources")?;
68
+
69
+        // Get primary output
70
+        let primary = conn
71
+            .randr_get_output_primary(root)?
72
+            .reply()
73
+            .context("Failed to get primary output")?
74
+            .output;
75
+
76
+        let mut monitors = Vec::new();
77
+
78
+        // Iterate through outputs
79
+        for &output in &resources.outputs {
80
+            let output_info = match conn.randr_get_output_info(output, resources.config_timestamp) {
81
+                Ok(cookie) => match cookie.reply() {
82
+                    Ok(info) => info,
83
+                    Err(_) => continue,
84
+                },
85
+                Err(_) => continue,
86
+            };
87
+
88
+            // Skip disconnected outputs
89
+            if output_info.connection != randr::Connection::CONNECTED {
90
+                continue;
91
+            }
92
+
93
+            // Skip outputs without a CRTC (not displaying anything)
94
+            if output_info.crtc == 0 {
95
+                continue;
96
+            }
97
+
98
+            // Get CRTC info for position and size
99
+            let crtc_info = match conn.randr_get_crtc_info(output_info.crtc, resources.config_timestamp) {
100
+                Ok(cookie) => match cookie.reply() {
101
+                    Ok(info) => info,
102
+                    Err(_) => continue,
103
+                },
104
+                Err(_) => continue,
105
+            };
106
+
107
+            // Skip CRTCs with no mode (disabled)
108
+            if crtc_info.mode == 0 {
109
+                continue;
110
+            }
111
+
112
+            let name = String::from_utf8_lossy(&output_info.name).to_string();
113
+            let is_primary = output == primary;
114
+
115
+            monitors.push(Monitor {
116
+                name: name.clone(),
117
+                x: crtc_info.x,
118
+                y: crtc_info.y,
119
+                width: crtc_info.width,
120
+                height: crtc_info.height,
121
+                primary: is_primary,
122
+            });
123
+
124
+            tracing::debug!(
125
+                name,
126
+                x = crtc_info.x,
127
+                y = crtc_info.y,
128
+                width = crtc_info.width,
129
+                height = crtc_info.height,
130
+                is_primary,
131
+                "Detected monitor"
132
+            );
133
+        }
134
+
135
+        // Calculate total virtual screen size
136
+        let total_width = monitors
137
+            .iter()
138
+            .map(|m| m.x as u32 + m.width as u32)
139
+            .max()
140
+            .unwrap_or(0) as u16;
141
+
142
+        let total_height = monitors
143
+            .iter()
144
+            .map(|m| m.y as u32 + m.height as u32)
145
+            .max()
146
+            .unwrap_or(0) as u16;
147
+
148
+        // Sort monitors: primary first, then by x position
149
+        monitors.sort_by(|a, b| {
150
+            if a.primary != b.primary {
151
+                b.primary.cmp(&a.primary) // primary first
152
+            } else {
153
+                a.x.cmp(&b.x) // then by x position
154
+            }
155
+        });
156
+
157
+        tracing::info!(
158
+            count = monitors.len(),
159
+            total_width,
160
+            total_height,
161
+            "Monitors detected"
162
+        );
163
+
164
+        Ok(Self {
165
+            monitors,
166
+            total_width,
167
+            total_height,
168
+        })
169
+    }
170
+
171
+    /// Get the primary monitor (or first monitor if no primary)
172
+    pub fn primary(&self) -> Option<&Monitor> {
173
+        self.monitors.iter().find(|m| m.primary).or(self.monitors.first())
174
+    }
175
+
176
+    /// Check if this is a single-monitor setup
177
+    pub fn is_single_monitor(&self) -> bool {
178
+        self.monitors.len() <= 1
179
+    }
180
+}
garlock/src/x11/window.rsadded
@@ -0,0 +1,295 @@
1
+//! Fullscreen locker window with secure input grabs
2
+//!
3
+//! Creates an override-redirect window that covers all monitors
4
+//! and grabs keyboard/pointer to prevent escape.
5
+
6
+use anyhow::{Context, Result};
7
+use std::time::Duration;
8
+use x11rb::connection::Connection;
9
+use x11rb::protocol::xproto::*;
10
+use x11rb::rust_connection::RustConnection;
11
+use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
12
+use x11rb::CURRENT_TIME;
13
+
14
+use super::monitors::MonitorConfig;
15
+
16
+/// Fullscreen locker window
17
+pub struct LockerWindow {
18
+    conn: RustConnection,
19
+    screen_num: usize,
20
+    window: Window,
21
+    gc: Gcontext,
22
+    width: u16,
23
+    height: u16,
24
+    depth: u8,
25
+}
26
+
27
+impl LockerWindow {
28
+    /// Create a new fullscreen locker window
29
+    ///
30
+    /// This will:
31
+    /// 1. Create an override-redirect fullscreen window
32
+    /// 2. Grab the keyboard (retrying until successful)
33
+    /// 3. Grab the pointer
34
+    pub fn new() -> Result<Self> {
35
+        let (conn, screen_num) =
36
+            x11rb::connect(None).context("Failed to connect to X server")?;
37
+
38
+        let screen = &conn.setup().roots[screen_num];
39
+        let width = screen.width_in_pixels;
40
+        let height = screen.height_in_pixels;
41
+        let root = screen.root;
42
+        let depth = screen.root_depth;
43
+        let visual = screen.root_visual;
44
+
45
+        tracing::info!(width, height, "Connected to X server");
46
+
47
+        // Create fullscreen window
48
+        let window = conn.generate_id().context("Failed to generate window ID")?;
49
+        conn.create_window(
50
+            depth,
51
+            window,
52
+            root,
53
+            0,
54
+            0,
55
+            width,
56
+            height,
57
+            0, // border_width
58
+            WindowClass::INPUT_OUTPUT,
59
+            visual,
60
+            &CreateWindowAux::new()
61
+                .background_pixel(screen.black_pixel)
62
+                .override_redirect(1) // Bypass window manager
63
+                .event_mask(
64
+                    EventMask::EXPOSURE
65
+                        | EventMask::KEY_PRESS
66
+                        | EventMask::KEY_RELEASE
67
+                        | EventMask::BUTTON_PRESS
68
+                        | EventMask::BUTTON_RELEASE
69
+                        | EventMask::POINTER_MOTION
70
+                        | EventMask::STRUCTURE_NOTIFY,
71
+                ),
72
+        )
73
+        .context("Failed to create window")?;
74
+
75
+        // Set fullscreen hints (for compositors that respect them)
76
+        Self::set_fullscreen_hints(&conn, window)?;
77
+
78
+        // Create graphics context for rendering
79
+        let gc = conn.generate_id().context("Failed to generate GC ID")?;
80
+        conn.create_gc(gc, window, &CreateGCAux::new())
81
+            .context("Failed to create GC")?;
82
+
83
+        // Map the window
84
+        conn.map_window(window).context("Failed to map window")?;
85
+        conn.flush().context("Failed to flush after map")?;
86
+
87
+        // Grab keyboard - CRITICAL for security
88
+        // Must retry because another application might have a grab
89
+        Self::grab_keyboard(&conn, window)?;
90
+
91
+        // Grab pointer - prevents clicking through to other windows
92
+        Self::grab_pointer(&conn, window)?;
93
+
94
+        // Raise window to top and focus
95
+        conn.configure_window(window, &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE))?;
96
+        conn.set_input_focus(InputFocus::POINTER_ROOT, window, CURRENT_TIME)?;
97
+        conn.flush()?;
98
+
99
+        tracing::info!("Locker window created and input grabbed");
100
+
101
+        Ok(Self {
102
+            conn,
103
+            screen_num,
104
+            window,
105
+            gc,
106
+            width,
107
+            height,
108
+            depth,
109
+        })
110
+    }
111
+
112
+    /// Set fullscreen window hints
113
+    fn set_fullscreen_hints(conn: &RustConnection, window: Window) -> Result<()> {
114
+        let net_wm_state = conn
115
+            .intern_atom(false, b"_NET_WM_STATE")
116
+            .context("Failed to intern _NET_WM_STATE")?
117
+            .reply()
118
+            .context("Failed to get _NET_WM_STATE reply")?
119
+            .atom;
120
+
121
+        let fullscreen = conn
122
+            .intern_atom(false, b"_NET_WM_STATE_FULLSCREEN")
123
+            .context("Failed to intern fullscreen atom")?
124
+            .reply()
125
+            .context("Failed to get fullscreen atom reply")?
126
+            .atom;
127
+
128
+        conn.change_property32(
129
+            PropMode::REPLACE,
130
+            window,
131
+            net_wm_state,
132
+            AtomEnum::ATOM,
133
+            &[fullscreen],
134
+        )
135
+        .context("Failed to set fullscreen property")?;
136
+
137
+        Ok(())
138
+    }
139
+
140
+    /// Grab keyboard with retry logic
141
+    fn grab_keyboard(conn: &RustConnection, window: Window) -> Result<()> {
142
+        const MAX_RETRIES: u32 = 100;
143
+        const RETRY_DELAY: Duration = Duration::from_millis(50);
144
+
145
+        for attempt in 0..MAX_RETRIES {
146
+            let reply = conn
147
+                .grab_keyboard(
148
+                    true, // owner_events
149
+                    window,
150
+                    CURRENT_TIME,
151
+                    GrabMode::ASYNC,
152
+                    GrabMode::ASYNC,
153
+                )?
154
+                .reply()
155
+                .context("Failed to get keyboard grab reply")?;
156
+
157
+            match reply.status {
158
+                GrabStatus::SUCCESS => {
159
+                    tracing::debug!(attempt, "Keyboard grab successful");
160
+                    return Ok(());
161
+                }
162
+                status => {
163
+                    tracing::trace!(?status, attempt, "Keyboard grab failed, retrying");
164
+                    std::thread::sleep(RETRY_DELAY);
165
+                }
166
+            }
167
+        }
168
+
169
+        anyhow::bail!("Failed to grab keyboard after {} retries", MAX_RETRIES)
170
+    }
171
+
172
+    /// Grab pointer to prevent clicking through
173
+    fn grab_pointer(conn: &RustConnection, window: Window) -> Result<()> {
174
+        let reply = conn
175
+            .grab_pointer(
176
+                true, // owner_events
177
+                window,
178
+                EventMask::BUTTON_PRESS | EventMask::POINTER_MOTION,
179
+                GrabMode::ASYNC,
180
+                GrabMode::ASYNC,
181
+                window,      // confine_to
182
+                x11rb::NONE, // cursor (use default)
183
+                CURRENT_TIME,
184
+            )?
185
+            .reply()
186
+            .context("Failed to get pointer grab reply")?;
187
+
188
+        match reply.status {
189
+            GrabStatus::SUCCESS => {
190
+                tracing::debug!("Pointer grab successful");
191
+                Ok(())
192
+            }
193
+            status => {
194
+                anyhow::bail!("Failed to grab pointer: {:?}", status)
195
+            }
196
+        }
197
+    }
198
+
199
+    /// Get window width
200
+    pub fn width(&self) -> u16 {
201
+        self.width
202
+    }
203
+
204
+    /// Get window height
205
+    pub fn height(&self) -> u16 {
206
+        self.height
207
+    }
208
+
209
+    /// Get window depth
210
+    pub fn depth(&self) -> u8 {
211
+        self.depth
212
+    }
213
+
214
+    /// Get the X11 connection
215
+    pub fn conn(&self) -> &RustConnection {
216
+        &self.conn
217
+    }
218
+
219
+    /// Get the window ID
220
+    pub fn window(&self) -> Window {
221
+        self.window
222
+    }
223
+
224
+    /// Get the root window ID
225
+    pub fn root(&self) -> Window {
226
+        self.conn.setup().roots[self.screen_num].root
227
+    }
228
+
229
+    /// Put an ARGB image to the window
230
+    /// Splits large images into chunks to avoid exceeding X11 request limits
231
+    pub fn put_image(&self, data: &[u8]) -> Result<()> {
232
+        let bytes_per_row = self.width as usize * 4;
233
+        let total_rows = self.height as usize;
234
+
235
+        // X11 max request is typically 4MB, use 1MB chunks to be safe
236
+        const MAX_CHUNK_BYTES: usize = 1024 * 1024;
237
+        let rows_per_chunk = (MAX_CHUNK_BYTES / bytes_per_row).max(1);
238
+
239
+        let mut y_offset: i16 = 0;
240
+        let mut remaining_rows = total_rows;
241
+        let mut data_offset = 0;
242
+
243
+        while remaining_rows > 0 {
244
+            let chunk_rows = remaining_rows.min(rows_per_chunk);
245
+            let chunk_bytes = chunk_rows * bytes_per_row;
246
+            let chunk_data = &data[data_offset..data_offset + chunk_bytes];
247
+
248
+            self.conn
249
+                .put_image(
250
+                    ImageFormat::Z_PIXMAP,
251
+                    self.window,
252
+                    self.gc,
253
+                    self.width,
254
+                    chunk_rows as u16,
255
+                    0,
256
+                    y_offset,
257
+                    0,
258
+                    self.depth,
259
+                    chunk_data,
260
+                )
261
+                .context("Failed to put image chunk")?;
262
+
263
+            y_offset += chunk_rows as i16;
264
+            remaining_rows -= chunk_rows;
265
+            data_offset += chunk_bytes;
266
+        }
267
+
268
+        self.conn.flush().context("Failed to flush after put_image")?;
269
+        Ok(())
270
+    }
271
+
272
+    /// Poll for event without blocking
273
+    pub fn poll_for_event(&self) -> Result<Option<x11rb::protocol::Event>> {
274
+        self.conn
275
+            .poll_for_event()
276
+            .context("Failed to poll for X11 event")
277
+    }
278
+
279
+    /// Get monitor configuration using RandR
280
+    pub fn get_monitors(&self) -> Result<MonitorConfig> {
281
+        MonitorConfig::detect(&self.conn, self.root())
282
+    }
283
+}
284
+
285
+impl Drop for LockerWindow {
286
+    fn drop(&mut self) {
287
+        // Release grabs
288
+        let _ = self.conn.ungrab_keyboard(CURRENT_TIME);
289
+        let _ = self.conn.ungrab_pointer(CURRENT_TIME);
290
+        // Destroy window
291
+        let _ = self.conn.destroy_window(self.window);
292
+        let _ = self.conn.flush();
293
+        tracing::debug!("Locker window destroyed, grabs released");
294
+    }
295
+}