@@ -1,13 +1,15 @@ |
| 1 | 1 | use anyhow::{Context, Result}; |
| 2 | | -use std::io::{BufRead, BufReader, Write}; |
| 2 | +use std::io::{BufRead, BufReader, ErrorKind, Write}; |
| 3 | 3 | use std::os::unix::fs::{MetadataExt, PermissionsExt}; |
| 4 | 4 | use std::os::unix::net::UnixStream; |
| 5 | 5 | use std::path::{Path, PathBuf}; |
| 6 | 6 | use std::process::{Command, Stdio}; |
| 7 | +use std::time::Duration; |
| 7 | 8 | |
| 8 | 9 | pub const DEFAULT_HELPER_SOCKET: &str = "/run/polkit/agent-helper.socket"; |
| 9 | 10 | const HELPER_TRANSPORT_ENV: &str = "GARCARD_POLKIT_HELPER_TRANSPORT"; |
| 10 | 11 | const HELPER_SOCKET_PROTOCOL_ENV: &str = "GARCARD_POLKIT_SOCKET_PROTOCOL"; |
| 12 | +const SOCKET_FIRST_RESPONSE_TIMEOUT: Duration = Duration::from_millis(500); |
| 11 | 13 | |
| 12 | 14 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 13 | 15 | pub enum HelperOutcome { |
@@ -84,7 +86,6 @@ impl HelperSocketClient { |
| 84 | 86 | let username_line = sanitize_control_line(username); |
| 85 | 87 | let cookie_line = sanitize_control_line(cookie); |
| 86 | 88 | let transport = helper_transport_mode(); |
| 87 | | - let protocol = helper_socket_protocol(); |
| 88 | 89 | tracing::debug!( |
| 89 | 90 | transport = %transport.as_str(), |
| 90 | 91 | env_key = HELPER_TRANSPORT_ENV, |
@@ -104,6 +105,67 @@ impl HelperSocketClient { |
| 104 | 105 | ); |
| 105 | 106 | } |
| 106 | 107 | |
| 108 | + match helper_socket_protocol() { |
| 109 | + HelperSocketProtocol::Auto => { |
| 110 | + self.authenticate_via_socket_auto(&username_line, &cookie_line, prompts) |
| 111 | + } |
| 112 | + protocol => { |
| 113 | + match self.authenticate_via_socket(&username_line, &cookie_line, prompts, protocol) |
| 114 | + { |
| 115 | + Ok(outcome) => Ok(outcome), |
| 116 | + Err(err) if is_no_session_cookie_error(&err) => { |
| 117 | + prompts |
| 118 | + .auth_failed("Authentication failed") |
| 119 | + .context("prompt failure callback failed")?; |
| 120 | + Ok(HelperOutcome::Denied) |
| 121 | + } |
| 122 | + Err(err) => Err(err), |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + fn authenticate_via_socket_auto<P: PromptProvider>( |
| 129 | + &self, |
| 130 | + username: &str, |
| 131 | + cookie: &str, |
| 132 | + prompts: &mut P, |
| 133 | + ) -> Result<HelperOutcome> { |
| 134 | + let protocols = [ |
| 135 | + HelperSocketProtocol::UsernameCookie, |
| 136 | + HelperSocketProtocol::CookieOnly, |
| 137 | + ]; |
| 138 | + |
| 139 | + for protocol in protocols { |
| 140 | + match self.authenticate_via_socket(username, cookie, prompts, protocol) { |
| 141 | + Ok(outcome) => return Ok(outcome), |
| 142 | + Err(err) |
| 143 | + if is_no_session_cookie_error(&err) |
| 144 | + || is_no_initial_helper_response_error(&err) => |
| 145 | + { |
| 146 | + tracing::warn!( |
| 147 | + protocol = %protocol.as_str(), |
| 148 | + error = %err, |
| 149 | + "Socket helper protocol attempt failed; retrying with alternate socket protocol" |
| 150 | + ); |
| 151 | + } |
| 152 | + Err(err) => return Err(err), |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + prompts |
| 157 | + .auth_failed("Authentication failed") |
| 158 | + .context("prompt failure callback failed")?; |
| 159 | + Ok(HelperOutcome::Denied) |
| 160 | + } |
| 161 | + |
| 162 | + fn authenticate_via_socket<P: PromptProvider>( |
| 163 | + &self, |
| 164 | + username_line: &str, |
| 165 | + cookie_line: &str, |
| 166 | + prompts: &mut P, |
| 167 | + protocol: HelperSocketProtocol, |
| 168 | + ) -> Result<HelperOutcome> { |
| 107 | 169 | let mut stream = UnixStream::connect(&self.socket_path).with_context(|| { |
| 108 | 170 | format!( |
| 109 | 171 | "failed to connect to polkit helper socket at {}", |
@@ -119,18 +181,10 @@ impl HelperSocketClient { |
| 119 | 181 | protocol = %protocol.as_str(), |
| 120 | 182 | "Connected to polkit helper socket" |
| 121 | 183 | ); |
| 122 | | - if username_line.len() != username.len() || cookie_line.len() != cookie.len() { |
| 123 | | - tracing::debug!( |
| 124 | | - original_username_len = username.len(), |
| 125 | | - normalized_username_len = username_line.len(), |
| 126 | | - original_cookie_len = cookie.len(), |
| 127 | | - normalized_cookie_len = cookie_line.len(), |
| 128 | | - "Normalized helper auth control lines before send" |
| 129 | | - ); |
| 130 | | - } |
| 131 | 184 | let read_stream = stream |
| 132 | 185 | .try_clone() |
| 133 | 186 | .context("failed to clone helper socket stream")?; |
| 187 | + let _ = read_stream.set_read_timeout(Some(SOCKET_FIRST_RESPONSE_TIMEOUT)); |
| 134 | 188 | let mut reader = BufReader::new(read_stream); |
| 135 | 189 | |
| 136 | 190 | if matches!(protocol, HelperSocketProtocol::UsernameCookie) { |
@@ -139,14 +193,30 @@ impl HelperSocketClient { |
| 139 | 193 | write_line(&mut stream, &cookie_line).context("failed to send helper cookie")?; |
| 140 | 194 | |
| 141 | 195 | let mut saw_no_session_cookie = false; |
| 196 | + let mut saw_first_event = false; |
| 142 | 197 | loop { |
| 143 | 198 | let mut line = String::new(); |
| 144 | | - let bytes = reader |
| 145 | | - .read_line(&mut line) |
| 146 | | - .context("failed to read helper response line")?; |
| 199 | + let bytes = match reader.read_line(&mut line) { |
| 200 | + Ok(bytes) => bytes, |
| 201 | + Err(err) |
| 202 | + if !saw_first_event |
| 203 | + && matches!(err.kind(), ErrorKind::TimedOut | ErrorKind::WouldBlock) => |
| 204 | + { |
| 205 | + return Err(NoInitialHelperResponseError.into()); |
| 206 | + } |
| 207 | + Err(err) => { |
| 208 | + return Err( |
| 209 | + anyhow::Error::new(err).context("failed to read helper response line") |
| 210 | + ); |
| 211 | + } |
| 212 | + }; |
| 147 | 213 | if bytes == 0 { |
| 148 | 214 | anyhow::bail!("helper closed connection unexpectedly"); |
| 149 | 215 | } |
| 216 | + if !saw_first_event { |
| 217 | + saw_first_event = true; |
| 218 | + let _ = reader.get_mut().set_read_timeout(None); |
| 219 | + } |
| 150 | 220 | tracing::debug!( |
| 151 | 221 | helper_line = %line.trim_end_matches('\n').trim_end_matches('\r'), |
| 152 | 222 | "Received helper protocol line" |
@@ -239,9 +309,7 @@ impl HelperSocketClient { |
| 239 | 309 | prompts, |
| 240 | 310 | ); |
| 241 | 311 | } |
| 242 | | - tracing::warn!( |
| 243 | | - "Socket helper reported no session for cookie and no viable direct helper is available; treating as authentication failure" |
| 244 | | - ); |
| 312 | + return Err(NoSessionForCookieError.into()); |
| 245 | 313 | } |
| 246 | 314 | prompts |
| 247 | 315 | .auth_failed("Authentication failed") |
@@ -378,6 +446,36 @@ impl HelperSocketClient { |
| 378 | 446 | } |
| 379 | 447 | } |
| 380 | 448 | |
| 449 | +#[derive(Debug)] |
| 450 | +struct NoSessionForCookieError; |
| 451 | + |
| 452 | +impl std::fmt::Display for NoSessionForCookieError { |
| 453 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 454 | + f.write_str("socket helper reported no session for cookie") |
| 455 | + } |
| 456 | +} |
| 457 | + |
| 458 | +impl std::error::Error for NoSessionForCookieError {} |
| 459 | + |
| 460 | +fn is_no_session_cookie_error(err: &anyhow::Error) -> bool { |
| 461 | + err.downcast_ref::<NoSessionForCookieError>().is_some() |
| 462 | +} |
| 463 | + |
| 464 | +#[derive(Debug)] |
| 465 | +struct NoInitialHelperResponseError; |
| 466 | + |
| 467 | +impl std::fmt::Display for NoInitialHelperResponseError { |
| 468 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 469 | + f.write_str("socket helper produced no initial response") |
| 470 | + } |
| 471 | +} |
| 472 | + |
| 473 | +impl std::error::Error for NoInitialHelperResponseError {} |
| 474 | + |
| 475 | +fn is_no_initial_helper_response_error(err: &anyhow::Error) -> bool { |
| 476 | + err.downcast_ref::<NoInitialHelperResponseError>().is_some() |
| 477 | +} |
| 478 | + |
| 381 | 479 | fn write_line(stream: &mut impl Write, value: &str) -> Result<()> { |
| 382 | 480 | stream.write_all(value.as_bytes())?; |
| 383 | 481 | stream.write_all(b"\n")?; |
@@ -467,6 +565,7 @@ impl HelperTransportMode { |
| 467 | 565 | |
| 468 | 566 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 469 | 567 | enum HelperSocketProtocol { |
| 568 | + Auto, |
| 470 | 569 | CookieOnly, |
| 471 | 570 | UsernameCookie, |
| 472 | 571 | } |
@@ -474,6 +573,7 @@ enum HelperSocketProtocol { |
| 474 | 573 | impl HelperSocketProtocol { |
| 475 | 574 | fn as_str(self) -> &'static str { |
| 476 | 575 | match self { |
| 576 | + Self::Auto => "socket-activated-auto", |
| 477 | 577 | Self::CookieOnly => "socket-activated-cookie-only", |
| 478 | 578 | Self::UsernameCookie => "socket-activated-username-cookie", |
| 479 | 579 | } |
@@ -497,10 +597,13 @@ fn helper_socket_protocol() -> HelperSocketProtocol { |
| 497 | 597 | .map(|value| value.trim().to_ascii_lowercase()) |
| 498 | 598 | .as_deref() |
| 499 | 599 | { |
| 600 | + Some("cookie-only") | Some("cookie_only") | Some("cookieonly") => { |
| 601 | + HelperSocketProtocol::CookieOnly |
| 602 | + } |
| 500 | 603 | Some("username-cookie") | Some("username_cookie") | Some("usernamecookie") => { |
| 501 | 604 | HelperSocketProtocol::UsernameCookie |
| 502 | 605 | } |
| 503 | | - _ => HelperSocketProtocol::CookieOnly, |
| 606 | + _ => HelperSocketProtocol::Auto, |
| 504 | 607 | } |
| 505 | 608 | } |
| 506 | 609 | |
@@ -683,8 +786,16 @@ mod tests { |
| 683 | 786 | let read_stream = stream.try_clone().expect("clone"); |
| 684 | 787 | let mut reader = BufReader::new(read_stream); |
| 685 | 788 | |
| 686 | | - let mut cookie = String::new(); |
| 687 | | - reader.read_line(&mut cookie).expect("read cookie"); |
| 789 | + let mut first_line = String::new(); |
| 790 | + reader.read_line(&mut first_line).expect("read first line"); |
| 791 | + let first = first_line.trim().to_string(); |
| 792 | + let cookie = if first == "alice" { |
| 793 | + let mut cookie = String::new(); |
| 794 | + reader.read_line(&mut cookie).expect("read cookie"); |
| 795 | + cookie.trim().to_string() |
| 796 | + } else { |
| 797 | + first |
| 798 | + }; |
| 688 | 799 | |
| 689 | 800 | stream |
| 690 | 801 | .write_all(b"PAM_PROMPT_ECHO_OFF Password:\n") |
@@ -696,7 +807,7 @@ mod tests { |
| 696 | 807 | |
| 697 | 808 | { |
| 698 | 809 | let mut lines = transcript_for_thread.lock().expect("lock transcript"); |
| 699 | | - lines.push(cookie.trim().to_string()); |
| 810 | + lines.push(cookie); |
| 700 | 811 | lines.push(secret.trim().to_string()); |
| 701 | 812 | } |
| 702 | 813 | |
@@ -734,8 +845,12 @@ mod tests { |
| 734 | 845 | let read_stream = stream.try_clone().expect("clone"); |
| 735 | 846 | let mut reader = BufReader::new(read_stream); |
| 736 | 847 | |
| 737 | | - let mut cookie = String::new(); |
| 738 | | - reader.read_line(&mut cookie).expect("read cookie"); |
| 848 | + let mut first_line = String::new(); |
| 849 | + reader.read_line(&mut first_line).expect("read first line"); |
| 850 | + if first_line.trim() == "alice" { |
| 851 | + let mut cookie = String::new(); |
| 852 | + reader.read_line(&mut cookie).expect("read cookie"); |
| 853 | + } |
| 739 | 854 | |
| 740 | 855 | stream |
| 741 | 856 | .write_all(b"PAM_PROMPT_ECHO_OFF Password:\n") |