1
use std::{collections::HashMap, time::Duration};
2

            
3
use nix::unistd::gethostname;
4
use serde::{Deserialize, Serialize};
5
use zlink::ReplyError;
6

            
7
use crate::server::{
8
    polkit::PolkitAuthority,
9
    walld::tty_utils::{self, TtyError},
10
};
11

            
12
#[zlink::proxy("no.ntnu.pvv.roowho2.walld")]
13
pub trait VarlinkWalldClientProxy {
14
    /// Broadcast `message` to the tty of every logged in user, optionally restricted to `group`.
15
    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
    /// Send `message` to a the tty of a single user, optionally targeting a specific tty.
25
    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")]
36
pub 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)]
57
pub enum VarlinkWalldClientResponse {
58
    Wall(VarlinkWallResponse),
59
    Write(VarlinkWriteResponse),
60
}
61

            
62
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
63
pub struct VarlinkWallResponse {
64
    pub delivered: Vec<VarlinkWallDelivery>,
65
    pub failures: Vec<VarlinkWallDeliveryFailure>,
66
}
67

            
68
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69
pub struct VarlinkWallDelivery {
70
    pub user: String,
71
    pub tty: String,
72
}
73

            
74
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75
pub struct VarlinkWallDeliveryFailure {
76
    pub user: String,
77
    pub tty: String,
78
    pub reason: String,
79
}
80

            
81
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
82
pub struct VarlinkWriteResponse {
83
    /// The tty the message was actually delivered to.
84
    /// This is relevant when the caller didn't specify one, and it was picked automatically.
85
    pub tty: String,
86
}
87

            
88
#[derive(Debug, Clone, PartialEq, ReplyError)]
89
#[zlink(interface = "no.ntnu.pvv.roowho2.walld")]
90
pub enum VarlinkWalldClientError {
91
    /// The walld service is not enabled on the server.
92
    Disabled,
93
    /// Caller was not authorized by polkit to perform the request.
94
    NotAuthorized,
95
    /// The target user is not logged in anywhere.
96
    UserNotLoggedIn { user: String },
97
    /// The target user has disabled incoming messages on the relevant tty/ttys.
98
    MessagesDisabled { user: String },
99
    /// The requested tty does not belong to the target user, or does not exist.
100
    TtyNotFound { user: String, tty: String },
101
    /// The request parameters were invalid (e.g. unknown group).
102
    InvalidRequest { message: String },
103
    /// The request timed out.
104
    TimedOut,
105
    /// Something went wrong while talking to the tty (open/write failure).
106
    Io { message: String },
107
}
108

            
109
// TODO: use `thiserror` to derive `Display` the impl instead.
110

            
111
impl 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

            
138
fn 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

            
144
fn hostname() -> String {
145
    gethostname()
146
        .ok()
147
        .and_then(|h| h.into_string().ok())
148
        .unwrap_or_else(|| "unknown".to_string())
149
}
150

            
151
/// Verifies that `source_tty` (if given) belongs to `source_uid`.
152
fn 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

            
167
fn 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

            
184
const POLKIT_ACTION_WALL: &str = "no.ntnu.pvv.roowho2.wall";
185
const POLKIT_ACTION_WRITE: &str = "no.ntnu.pvv.roowho2.write";
186

            
187
/// Check whether `pid` is allowed to broadcast a `wall` message, optionally restricted to `group`.
188
async 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

            
200
/// Check whether `pid` is allowed to `write` to `user`, optionally on a specific `tty`.
201
async 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

            
215
/// Wrapper doing some basic error handling against a polkit check.
216
async fn authorize<'a, F, Fut>(
217
    polkit: Option<&'a PolkitAuthority>,
218
    check: F,
219
) -> Result<(), VarlinkWalldClientError>
220
where
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)]
240
pub 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

            
294
const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(30);
295

            
296
pub 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
        // No tty requested, pick the one the user touched most recently
328
        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
}