gardesk/garwarp / 41ae72d

Browse files

validate request identity

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
41ae72df275831f5f3b15933f45f9febf095fcfa
Parents
0f82106
Tree
8a53de7

3 changed files

StatusFile+-
M garwarp/src/daemon.rs 83 0
M garwarp/src/main.rs 1 0
A garwarp/src/validate.rs 78 0
garwarp/src/daemon.rsmodified
@@ -16,6 +16,7 @@ use crate::logging;
1616
 use crate::request::{RequestOwner, RequestRegistry, RequestState};
1717
 use crate::request_store;
1818
 use crate::runtime::RuntimePaths;
19
+use crate::validate::validate_request_identity;
1920
 use crate::window::parse_optional_parent_window;
2021
 
2122
 pub fn run() -> io::Result<()> {
@@ -118,6 +119,18 @@ fn handle_connection(stream: UnixStream, state: &mut DaemonState) -> io::Result<
118119
             app_id,
119120
             parent_window,
120121
         }) => {
122
+            let validation = validate_request_identity(&id, &sender, app_id.as_deref());
123
+            if let Err(error) = validation {
124
+                let mapping = map_portal_error(&error);
125
+                return write_response(
126
+                    reader.into_inner(),
127
+                    ControlResponse::Error {
128
+                        code: mapping.code as u32,
129
+                        reason: mapping.reason.to_string(),
130
+                    },
131
+                );
132
+            }
133
+
121134
             let owner = RequestOwner::new(sender, app_id);
122135
             let parsed_parent_window = match parse_optional_parent_window(parent_window.as_deref())
123136
             {
@@ -157,6 +170,18 @@ fn handle_connection(stream: UnixStream, state: &mut DaemonState) -> io::Result<
157170
             app_id,
158171
             target,
159172
         }) => {
173
+            let validation = validate_request_identity(&id, &sender, app_id.as_deref());
174
+            if let Err(error) = validation {
175
+                let mapping = map_portal_error(&error);
176
+                return write_response(
177
+                    reader.into_inner(),
178
+                    ControlResponse::Error {
179
+                        code: mapping.code as u32,
180
+                        reason: mapping.reason.to_string(),
181
+                    },
182
+                );
183
+            }
184
+
160185
             let owner = RequestOwner::new(sender, app_id);
161186
             let target_state = map_transition_target(target);
162187
             match state.requests.transition(&id, &owner, target_state) {
@@ -410,6 +435,64 @@ mod tests {
410435
         );
411436
     }
412437
 
438
+    #[test]
439
+    fn invalid_request_id_maps_to_invalid_request() {
440
+        let (mut client, server) = UnixStream::pair().expect("pair should be created");
441
+        client
442
+            .write_all(b"begin id=req/1 sender=:1.2 parent=x11:0x2a\n")
443
+            .expect("begin request should be written");
444
+
445
+        let mut state = DaemonState {
446
+            health: HealthStatus::Healthy,
447
+            requests: RequestRegistry::new(Duration::from_secs(5)),
448
+            running: true,
449
+        };
450
+        handle_connection(server, &mut state).expect("begin should be handled");
451
+
452
+        let mut response_line = String::new();
453
+        let mut reader = BufReader::new(client);
454
+        reader
455
+            .read_line(&mut response_line)
456
+            .expect("response should be readable");
457
+        let response = ControlResponse::parse_line(&response_line).expect("response should parse");
458
+        assert_eq!(
459
+            response,
460
+            ControlResponse::Error {
461
+                code: 2,
462
+                reason: "invalid_request".to_string(),
463
+            }
464
+        );
465
+    }
466
+
467
+    #[test]
468
+    fn invalid_sender_maps_to_invalid_request() {
469
+        let (mut client, server) = UnixStream::pair().expect("pair should be created");
470
+        client
471
+            .write_all(b"transition id=req-1 sender=org.test.App state=cancelled\n")
472
+            .expect("transition request should be written");
473
+
474
+        let mut state = DaemonState {
475
+            health: HealthStatus::Healthy,
476
+            requests: RequestRegistry::new(Duration::from_secs(5)),
477
+            running: true,
478
+        };
479
+        handle_connection(server, &mut state).expect("transition should be handled");
480
+
481
+        let mut response_line = String::new();
482
+        let mut reader = BufReader::new(client);
483
+        reader
484
+            .read_line(&mut response_line)
485
+            .expect("response should be readable");
486
+        let response = ControlResponse::parse_line(&response_line).expect("response should parse");
487
+        assert_eq!(
488
+            response,
489
+            ControlResponse::Error {
490
+                code: 2,
491
+                reason: "invalid_request".to_string(),
492
+            }
493
+        );
494
+    }
495
+
413496
     #[test]
414497
     fn transition_owner_mismatch_maps_to_stable_reason() {
415498
         let (mut client, server) = UnixStream::pair().expect("pair should be created");
garwarp/src/main.rsmodified
@@ -7,6 +7,7 @@ mod logging;
77
 mod request;
88
 mod request_store;
99
 mod runtime;
10
+mod validate;
1011
 mod window;
1112
 
1213
 use std::env;
garwarp/src/validate.rsadded
@@ -0,0 +1,78 @@
1
+use crate::error::PortalError;
2
+
3
+const MAX_REQUEST_ID_LEN: usize = 128;
4
+const MAX_SENDER_LEN: usize = 128;
5
+const MAX_APP_ID_LEN: usize = 255;
6
+
7
+pub fn validate_request_identity(
8
+    request_id: &str,
9
+    sender: &str,
10
+    app_id: Option<&str>,
11
+) -> Result<(), PortalError> {
12
+    if !is_valid_request_id(request_id) {
13
+        return Err(PortalError::InvalidRequestPayload);
14
+    }
15
+    if !is_valid_sender(sender) {
16
+        return Err(PortalError::InvalidRequestPayload);
17
+    }
18
+    if let Some(app_id) = app_id
19
+        && !is_valid_app_id(app_id)
20
+    {
21
+        return Err(PortalError::InvalidRequestPayload);
22
+    }
23
+    Ok(())
24
+}
25
+
26
+fn is_valid_request_id(value: &str) -> bool {
27
+    !value.is_empty()
28
+        && value.len() <= MAX_REQUEST_ID_LEN
29
+        && value
30
+            .chars()
31
+            .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-' | ':'))
32
+}
33
+
34
+fn is_valid_sender(value: &str) -> bool {
35
+    value.len() <= MAX_SENDER_LEN
36
+        && value.starts_with(':')
37
+        && value
38
+            .chars()
39
+            .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-' | ':'))
40
+}
41
+
42
+fn is_valid_app_id(value: &str) -> bool {
43
+    !value.is_empty()
44
+        && value.len() <= MAX_APP_ID_LEN
45
+        && value.contains('.')
46
+        && value
47
+            .chars()
48
+            .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-'))
49
+}
50
+
51
+#[cfg(test)]
52
+mod tests {
53
+    use super::validate_request_identity;
54
+
55
+    #[test]
56
+    fn accepts_valid_identity() {
57
+        let result = validate_request_identity("req-1", ":1.2", Some("org.test.App"));
58
+        assert!(result.is_ok());
59
+    }
60
+
61
+    #[test]
62
+    fn rejects_invalid_request_id() {
63
+        let result = validate_request_identity("req/1", ":1.2", Some("org.test.App"));
64
+        assert!(result.is_err());
65
+    }
66
+
67
+    #[test]
68
+    fn rejects_invalid_sender() {
69
+        let result = validate_request_identity("req-1", "org.test.App", Some("org.test.App"));
70
+        assert!(result.is_err());
71
+    }
72
+
73
+    #[test]
74
+    fn rejects_invalid_app_id() {
75
+        let result = validate_request_identity("req-1", ":1.2", Some("bad$app"));
76
+        assert!(result.is_err());
77
+    }
78
+}