1use std::{collections::HashMap, time::Duration};
2
3use nix::unistd::gethostname;
4use serde::{Deserialize, Serialize};
5use zlink::ReplyError;
6
7use crate::server::{
8 polkit::PolkitAuthority,
9 walld::tty_utils::{self, TtyError},
10};
11
12#[zlink::proxy("no.ntnu.pvv.roowho2.walld")]
13pub trait VarlinkWalldClientProxy {
14 async fn wall(
16 &mut self,
17 source_tty: Option<String>,
18 message: String,
19 group: Option<String>,
20 nobanner: bool,
21 timeout_secs: u32,
22 ) -> zlink::Result<Result<VarlinkWalldClientResponse, VarlinkWalldClientError>>;
23
24 async fn write(
26 &mut self,
27 source_tty: Option<String>,
28 target_user: String,
29 target_tty: Option<String>,
30 message: String,
31 ) -> zlink::Result<Result<VarlinkWalldClientResponse, VarlinkWalldClientError>>;
32}
33
34#[derive(Debug, Deserialize)]
35#[serde(tag = "method", content = "parameters")]
36pub enum VarlinkWalldClientRequest {
37 #[serde(rename = "no.ntnu.pvv.roowho2.walld.Wall")]
38 Wall {
39 source_tty: Option<String>,
40 message: String,
41 group: Option<String>,
42 nobanner: bool,
43 timeout_secs: u32,
44 },
45
46 #[serde(rename = "no.ntnu.pvv.roowho2.walld.Write")]
47 Write {
48 source_tty: Option<String>,
49 target_user: String,
50 target_tty: Option<String>,
51 message: String,
52 },
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56#[serde(untagged)]
57pub enum VarlinkWalldClientResponse {
58 Wall(VarlinkWallResponse),
59 Write(VarlinkWriteResponse),
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
63pub struct VarlinkWallResponse {
64 pub delivered: Vec<VarlinkWallDelivery>,
65 pub failures: Vec<VarlinkWallDeliveryFailure>,
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct VarlinkWallDelivery {
70 pub user: String,
71 pub tty: String,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct VarlinkWallDeliveryFailure {
76 pub user: String,
77 pub tty: String,
78 pub reason: String,
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
82pub struct VarlinkWriteResponse {
83 pub tty: String,
86}
87
88#[derive(Debug, Clone, PartialEq, ReplyError)]
89#[zlink(interface = "no.ntnu.pvv.roowho2.walld")]
90pub enum VarlinkWalldClientError {
91 Disabled,
93 NotAuthorized,
95 UserNotLoggedIn { user: String },
97 MessagesDisabled { user: String },
99 TtyNotFound { user: String, tty: String },
101 InvalidRequest { message: String },
103 TimedOut,
105 Io { message: String },
107}
108
109impl std::fmt::Display for VarlinkWalldClientError {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 VarlinkWalldClientError::Disabled => {
115 write!(f, "The walld service is disabled on the server")
116 }
117 VarlinkWalldClientError::NotAuthorized => write!(f, "Caller is not authorized"),
118 VarlinkWalldClientError::UserNotLoggedIn { user } => {
119 write!(f, "User '{}' is not logged in", user)
120 }
121 VarlinkWalldClientError::MessagesDisabled { user } => {
122 write!(f, "User '{}' has disabled incoming messages", user)
123 }
124 VarlinkWalldClientError::TtyNotFound { user, tty } => {
125 write!(f, "Tty '{}' not found for user '{}'", tty, user)
126 }
127 VarlinkWalldClientError::InvalidRequest { message } => {
128 write!(f, "Invalid request: {}", message)
129 }
130 VarlinkWalldClientError::TimedOut => write!(f, "Request timed out"),
131 VarlinkWalldClientError::Io { message } => {
132 write!(f, "I/O error while sending message: {}", message)
133 }
134 }
135 }
136}
137
138fn username_for_uid(uid: u32) -> String {
139 users::get_user_by_uid(uid)
140 .map(|u| u.name().to_string_lossy().into_owned())
141 .unwrap_or_else(|| uid.to_string())
142}
143
144fn hostname() -> String {
145 gethostname()
146 .ok()
147 .and_then(|h| h.into_string().ok())
148 .unwrap_or_else(|| "unknown".to_string())
149}
150
151fn verify_source_tty(
153 source_tty: Option<&str>,
154 source_uid: u32,
155) -> Result<(), VarlinkWalldClientError> {
156 let Some(tty) = source_tty else {
157 return Ok(());
158 };
159
160 tty_utils::verify_tty_owner(tty, source_uid).map_err(|err| {
161 VarlinkWalldClientError::InvalidRequest {
162 message: format!("source_tty {tty:?}: {err}"),
163 }
164 })
165}
166
167fn tty_error_to_walld_error(user: &str, tty: &str, err: TtyError) -> VarlinkWalldClientError {
168 match err {
169 TtyError::NotFound(_) | TtyError::InvalidName(_) | TtyError::NotCharacterDevice(_) => {
170 VarlinkWalldClientError::TtyNotFound {
171 user: user.to_string(),
172 tty: tty.to_string(),
173 }
174 }
175 TtyError::Unavailable(_) => VarlinkWalldClientError::MessagesDisabled {
176 user: user.to_string(),
177 },
178 other => VarlinkWalldClientError::Io {
179 message: other.to_string(),
180 },
181 }
182}
183
184const POLKIT_ACTION_WALL: &str = "no.ntnu.pvv.roowho2.wall";
185const POLKIT_ACTION_WRITE: &str = "no.ntnu.pvv.roowho2.write";
186
187async fn check_wall(
189 polkit: &PolkitAuthority,
190 pid: u32,
191 group: Option<&str>,
192) -> anyhow::Result<bool> {
193 let mut details = HashMap::new();
194 if let Some(group) = group {
195 details.insert("group", group);
196 }
197 polkit.check(pid, POLKIT_ACTION_WALL, details).await
198}
199
200async fn check_write(
202 polkit: &PolkitAuthority,
203 pid: u32,
204 user: &str,
205 tty: Option<&str>,
206) -> anyhow::Result<bool> {
207 let mut details = HashMap::new();
208 details.insert("user", user);
209 if let Some(tty) = tty {
210 details.insert("tty", tty);
211 }
212 polkit.check(pid, POLKIT_ACTION_WRITE, details).await
213}
214
215async fn authorize<'a, F, Fut>(
217 polkit: Option<&'a PolkitAuthority>,
218 check: F,
219) -> Result<(), VarlinkWalldClientError>
220where
221 F: FnOnce(&'a PolkitAuthority) -> Fut,
222 Fut: std::future::Future<Output = anyhow::Result<bool>>,
223{
224 let Some(polkit) = polkit else {
225 tracing::error!("polkit authority is unavailable; denying request");
226 return Err(VarlinkWalldClientError::NotAuthorized);
227 };
228
229 match check(polkit).await {
230 Ok(true) => Ok(()),
231 Ok(false) => Err(VarlinkWalldClientError::NotAuthorized),
232 Err(err) => {
233 tracing::error!("polkit authorization check failed: {err:#}");
234 Err(VarlinkWalldClientError::NotAuthorized)
235 }
236 }
237}
238
239#[allow(clippy::too_many_arguments)]
240pub async fn handle_wall_request(
241 polkit: Option<&PolkitAuthority>,
242 source_pid: u32,
243 source_uid: u32,
244 source_tty: Option<String>,
245 message: String,
246 group: Option<String>,
247 nobanner: bool,
248 timeout_secs: u32,
249) -> Result<VarlinkWallResponse, VarlinkWalldClientError> {
250 if let Some(group) = &group
251 && users::get_group_by_name(group).is_none()
252 {
253 return Err(VarlinkWalldClientError::InvalidRequest {
254 message: format!("unknown group {group:?}"),
255 });
256 }
257
258 verify_source_tty(source_tty.as_deref(), source_uid)?;
259
260 authorize(polkit, |authority| {
261 check_wall(authority, source_pid, group.as_deref())
262 })
263 .await?;
264
265 let body = tty_utils::format_wall_message(
266 &username_for_uid(source_uid),
267 &hostname(),
268 source_tty.as_deref(),
269 &message,
270 nobanner,
271 );
272
273 let timeout = Duration::from_secs(timeout_secs.max(1) as u64);
274 let sessions = tty_utils::all_sessions(group.as_deref());
275
276 let mut response = VarlinkWallResponse::default();
277 for session in sessions {
278 match tty_utils::deliver_message(&session.tty, &body, timeout).await {
279 Ok(()) => response.delivered.push(VarlinkWallDelivery {
280 user: session.user,
281 tty: session.tty,
282 }),
283 Err(err) => response.failures.push(VarlinkWallDeliveryFailure {
284 user: session.user,
285 tty: session.tty,
286 reason: err.to_string(),
287 }),
288 }
289 }
290
291 Ok(response)
292}
293
294const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(30);
295
296pub async fn handle_write_request(
297 polkit: Option<&PolkitAuthority>,
298 source_pid: u32,
299 source_uid: u32,
300 source_tty: Option<String>,
301 user: String,
302 tty: Option<String>,
303 message: String,
304) -> Result<VarlinkWriteResponse, VarlinkWalldClientError> {
305 verify_source_tty(source_tty.as_deref(), source_uid)?;
306
307 authorize(polkit, |authority| {
308 check_write(authority, source_pid, &user, tty.as_deref())
309 })
310 .await?;
311
312 let sessions = tty_utils::sessions_for_user(&user);
313 if sessions.is_empty() {
314 return Err(VarlinkWalldClientError::UserNotLoggedIn { user });
315 }
316
317 let target_tty = match &tty {
318 Some(tty) => {
319 if !sessions.iter().any(|s| &s.tty == tty) {
320 return Err(VarlinkWalldClientError::TtyNotFound {
321 user,
322 tty: tty.clone(),
323 });
324 }
325 tty.clone()
326 }
327 None => sessions
329 .iter()
330 .filter_map(|s| {
331 tty_utils::stat_tty(&s.tty)
332 .ok()
333 .map(|info| (s.tty.clone(), info))
334 })
335 .max_by_key(|(_, info)| info.atime)
336 .map(|(tty, _)| tty)
337 .ok_or_else(|| VarlinkWalldClientError::MessagesDisabled { user: user.clone() })?,
338 };
339
340 match tty_utils::stat_tty(&target_tty) {
341 Ok(info) if info.writable => {}
342 Ok(_) => return Err(VarlinkWalldClientError::MessagesDisabled { user }),
343 Err(_) => {
344 return Err(VarlinkWalldClientError::TtyNotFound {
345 user,
346 tty: target_tty,
347 });
348 }
349 }
350
351 let source_user = username_for_uid(source_uid);
352 let body =
353 tty_utils::format_write_message(&source_user, &hostname(), source_tty.as_deref(), &message);
354
355 tty_utils::deliver_message(&target_tty, &body, DEFAULT_WRITE_TIMEOUT)
356 .await
357 .map_err(|err| tty_error_to_walld_error(&user, &target_tty, err))?;
358
359 Ok(VarlinkWriteResponse { tty: target_tty })
360}