Skip to main content

roowho2_lib/server/
varlink_api.rs

1mod fingerd;
2mod rwhod;
3mod walld;
4
5use std::{os::fd::OwnedFd, time::Duration};
6
7use anyhow::Context;
8use futures_util::stream;
9use serde::{Deserialize, Serialize};
10use tokio::time::timeout;
11use zlink::{
12    service::MethodReply,
13    tokio::unix::{Listener as UnixListener, Stream as UnixStream},
14};
15
16use crate::server::{ignore_list::IgnoreList, polkit::PolkitAuthority, rwhod::RwhodStatusStore};
17
18pub use crate::server::varlink_api::{fingerd::*, rwhod::*, walld::*};
19
20pub const DEFAULT_CLIENT_SERVER_SOCKET_PATH: &str = "/run/roowho2/roowho2.varlink";
21
22macro_rules! require_enabled {
23    ($self:ident, $flag:ident, $reply_variant:ident, $error_ty:ident) => {
24        if !$self.$flag {
25            return (
26                MethodReply::Error(VarlinkReplyError::$reply_variant($error_ty::Disabled)),
27                Default::default(),
28            );
29        }
30    };
31}
32
33macro_rules! with_timeout {
34    ($future:expr, $name:literal, $reply_variant:ident, $error_ty:ident) => {
35        match timeout(Duration::from_secs(2), $future).await {
36            Ok(response) => response,
37            Err(_) => {
38                tracing::error!(concat!($name, " request timed out after 2 seconds"));
39                return (
40                    MethodReply::Error(VarlinkReplyError::$reply_variant($error_ty::TimedOut)),
41                    Default::default(),
42                );
43            }
44        }
45    };
46}
47
48macro_rules! reply_ok {
49    ($reply_variant:ident, $response_variant:path, $value:expr) => {
50        (
51            MethodReply::Single(Some(VarlinkReply::$reply_variant($response_variant(
52                $value,
53            )))),
54            Default::default(),
55        )
56    };
57}
58
59macro_rules! reply_result {
60    ($reply_variant:ident, $response_variant:path, $result:expr) => {
61        match $result {
62            Ok(response) => reply_ok!($reply_variant, $response_variant, response),
63            Err(err) => (
64                MethodReply::Error(VarlinkReplyError::$reply_variant(err)),
65                Default::default(),
66            ),
67        }
68    };
69}
70
71#[derive(Debug, Deserialize)]
72#[serde(untagged)]
73#[allow(unused)]
74pub enum VarlinkMethod {
75    Rwhod(VarlinkRwhodClientRequest),
76    Finger(VarlinkFingerClientRequest),
77    Walld(VarlinkWalldClientRequest),
78}
79
80#[derive(Debug, Serialize)]
81#[serde(untagged)]
82#[allow(unused)]
83pub enum VarlinkReply {
84    Rwhod(VarlinkRwhodClientResponse),
85    Finger(VarlinkFingerClientResponse),
86    Walld(VarlinkWalldClientResponse),
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize)]
90#[serde(untagged)]
91#[allow(unused)]
92pub enum VarlinkReplyError {
93    Rwhod(VarlinkRwhodClientError),
94    Finger(VarlinkFingerClientError),
95    Walld(VarlinkWalldClientError),
96}
97
98#[derive(Clone)]
99pub struct VarlinkRoowhoo2ClientServer {
100    whod_status_store: RwhodStatusStore,
101    rwhod_enabled: bool,
102    fingerd_enabled: bool,
103    walld_enabled: bool,
104    finger_ignore_list: Option<IgnoreList>,
105    polkit: Option<PolkitAuthority>,
106}
107
108impl VarlinkRoowhoo2ClientServer {
109    pub fn new(
110        whod_status_store: RwhodStatusStore,
111        rwhod_enabled: bool,
112        fingerd_enabled: bool,
113        walld_enabled: bool,
114        finger_ignore_list: Option<IgnoreList>,
115        polkit: Option<PolkitAuthority>,
116    ) -> Self {
117        Self {
118            whod_status_store,
119            rwhod_enabled,
120            fingerd_enabled,
121            walld_enabled,
122            finger_ignore_list,
123            polkit,
124        }
125    }
126}
127
128impl VarlinkRoowhoo2ClientServer {}
129
130impl zlink::Service<UnixStream> for VarlinkRoowhoo2ClientServer {
131    type MethodCall<'de> = VarlinkMethod;
132    type ReplyParams<'se> = VarlinkReply;
133    type ReplyError<'se> = VarlinkReplyError;
134    type ReplyStreamParams = ();
135    type ReplyStream = stream::Empty<(
136        Result<zlink::Reply<Self::ReplyStreamParams>, Self::ReplyStreamError>,
137        Vec<OwnedFd>,
138    )>;
139    type ReplyStreamError = ();
140
141    async fn handle<'service>(
142        &'service mut self,
143        call: &'service zlink::Call<Self::MethodCall<'_>>,
144        conn: &mut zlink::Connection<UnixStream>,
145        _fds: Vec<OwnedFd>,
146    ) -> zlink::service::HandleResult<
147        Self::ReplyParams<'service>,
148        Self::ReplyStream,
149        Self::ReplyError<'service>,
150    > {
151        let (peer_pid, peer_uid) = match conn.peer_credentials().await {
152            Ok(creds) => (
153                creds.process_id().as_raw_pid() as u32,
154                creds.unix_user_id().as_raw(),
155            ),
156            Err(e) => {
157                tracing::error!("failed to read peer credentials: {e}");
158                // TODO: peercreds are currently only used for walld, but this should be a more "toplevel"
159                //       error at some point when we start using peercred for other things as well.
160                return (
161                    MethodReply::Error(VarlinkReplyError::Walld(VarlinkWalldClientError::Io {
162                        message: "failed to identify caller".to_string(),
163                    })),
164                    Default::default(),
165                );
166            }
167        };
168
169        match call.method() {
170            VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Rwho { max_idle_seconds }) => {
171                require_enabled!(self, rwhod_enabled, Rwhod, VarlinkRwhodClientError);
172
173                let max_idle_time = match max_idle_seconds
174                    .map(|secs| chrono::TimeDelta::try_seconds(secs).ok_or(()))
175                    .transpose()
176                    .map_err(|_| {
177                        tracing::error!(
178                            "Received out-of-range max_idle_seconds: {:?}",
179                            max_idle_seconds
180                        );
181                        (
182                            MethodReply::Error(VarlinkReplyError::Rwhod(
183                                VarlinkRwhodClientError::InvalidRequest,
184                            )),
185                            Default::default(),
186                        )
187                    }) {
188                    Ok(max_idle_time) => max_idle_time,
189                    Err(err) => return err,
190                };
191
192                let result = with_timeout!(
193                    handle_rwho_request(&self.whod_status_store, max_idle_time),
194                    "Rwho",
195                    Rwhod,
196                    VarlinkRwhodClientError
197                );
198
199                reply_ok!(Rwhod, VarlinkRwhodClientResponse::Rwho, result)
200            }
201            VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Ruptime { max_idle_seconds }) => {
202                require_enabled!(self, rwhod_enabled, Rwhod, VarlinkRwhodClientError);
203
204                let max_idle_time = match max_idle_seconds
205                    .map(|secs| chrono::TimeDelta::try_seconds(secs).ok_or(()))
206                    .transpose()
207                    .map_err(|_| {
208                        tracing::error!(
209                            "Received out-of-range max_idle_seconds: {:?}",
210                            max_idle_seconds
211                        );
212                        (
213                            MethodReply::Error(VarlinkReplyError::Rwhod(
214                                VarlinkRwhodClientError::InvalidRequest,
215                            )),
216                            Default::default(),
217                        )
218                    }) {
219                    Ok(max_idle_time) => max_idle_time,
220                    Err(err) => return err,
221                };
222
223                let result = with_timeout!(
224                    handle_ruptime_request(&self.whod_status_store, max_idle_time),
225                    "Ruptime",
226                    Rwhod,
227                    VarlinkRwhodClientError
228                );
229
230                reply_ok!(Rwhod, VarlinkRwhodClientResponse::Ruptime, result)
231            }
232            VarlinkMethod::Finger(VarlinkFingerClientRequest::Finger {
233                user_queries,
234                match_fullnames,
235                request_info,
236                request_networking,
237                disable_user_account_db,
238                raw_remote_output,
239            }) => {
240                require_enabled!(self, fingerd_enabled, Finger, VarlinkFingerClientError);
241
242                let result = with_timeout!(
243                    handle_finger_request(
244                        &self.finger_ignore_list,
245                        user_queries.clone(),
246                        *match_fullnames,
247                        request_info.clone(),
248                        request_networking.clone(),
249                        *disable_user_account_db,
250                        *raw_remote_output,
251                    ),
252                    "Finger",
253                    Finger,
254                    VarlinkFingerClientError
255                );
256
257                reply_ok!(Finger, VarlinkFingerClientResponse::Finger, result)
258            }
259            VarlinkMethod::Walld(VarlinkWalldClientRequest::Wall {
260                source_tty,
261                message,
262                group,
263                nobanner,
264                timeout_secs,
265            }) => {
266                require_enabled!(self, walld_enabled, Walld, VarlinkWalldClientError);
267
268                // TODO: ensure the outer duration is more than the inner duration.
269                let result = with_timeout!(
270                    handle_wall_request(
271                        self.polkit.as_ref(),
272                        peer_pid,
273                        peer_uid,
274                        source_tty.clone(),
275                        message.clone(),
276                        group.clone(),
277                        *nobanner,
278                        *timeout_secs,
279                    ),
280                    "Wall",
281                    Walld,
282                    VarlinkWalldClientError
283                );
284
285                reply_result!(Walld, VarlinkWalldClientResponse::Wall, result)
286            }
287            VarlinkMethod::Walld(VarlinkWalldClientRequest::Write {
288                source_tty,
289                target_user,
290                target_tty,
291                message,
292            }) => {
293                require_enabled!(self, walld_enabled, Walld, VarlinkWalldClientError);
294
295                let result = with_timeout!(
296                    handle_write_request(
297                        self.polkit.as_ref(),
298                        peer_pid,
299                        peer_uid,
300                        source_tty.clone(),
301                        target_user.clone(),
302                        target_tty.clone(),
303                        message.clone(),
304                    ),
305                    "Write",
306                    Walld,
307                    VarlinkWalldClientError
308                );
309
310                reply_result!(Walld, VarlinkWalldClientResponse::Write, result)
311            }
312        }
313    }
314}
315
316pub async fn varlink_client_server_task(
317    socket: UnixListener,
318    whod_status_store: RwhodStatusStore,
319    rwhod_enabled: bool,
320    fingerd_enabled: bool,
321    walld_enabled: bool,
322    finger_ignore_list: Option<IgnoreList>,
323) -> anyhow::Result<()> {
324    let polkit = if !walld_enabled {
325        None
326    } else {
327        match timeout(Duration::from_secs(5), PolkitAuthority::connect()).await {
328            Ok(Ok(polkit)) => Some(polkit),
329            Ok(Err(e)) => {
330                tracing::warn!(
331                    "Failed to connect to polkit; Wall/Write requests will be denied: {e:#}"
332                );
333                None
334            }
335            Err(_) => {
336                tracing::warn!(
337                    "Timed out connecting to polkit after 5 seconds; Wall/Write requests will be denied"
338                );
339                None
340            }
341        }
342    };
343
344    let service = VarlinkRoowhoo2ClientServer::new(
345        whod_status_store,
346        rwhod_enabled,
347        fingerd_enabled,
348        walld_enabled,
349        finger_ignore_list,
350        polkit,
351    );
352
353    let server = zlink::Server::new(socket, service);
354
355    tracing::info!("Starting Rwhod client API server");
356
357    server
358        .run()
359        .await
360        .context("Rwhod client API server failed")?;
361
362    Ok(())
363}