1
mod fingerd;
2
mod rwhod;
3
mod walld;
4

            
5
use std::{os::fd::OwnedFd, time::Duration};
6

            
7
use anyhow::Context;
8
use futures_util::stream;
9
use serde::{Deserialize, Serialize};
10
use tokio::time::timeout;
11
use zlink::{
12
    service::MethodReply,
13
    tokio::unix::{Listener as UnixListener, Stream as UnixStream},
14
};
15

            
16
use crate::server::{ignore_list::IgnoreList, polkit::PolkitAuthority, rwhod::RwhodStatusStore};
17

            
18
pub use crate::server::varlink_api::{fingerd::*, rwhod::*, walld::*};
19

            
20
pub const DEFAULT_CLIENT_SERVER_SOCKET_PATH: &str = "/run/roowho2/roowho2.varlink";
21

            
22
macro_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

            
33
macro_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

            
48
macro_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

            
59
macro_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)]
74
pub enum VarlinkMethod {
75
    Rwhod(VarlinkRwhodClientRequest),
76
    Finger(VarlinkFingerClientRequest),
77
    Walld(VarlinkWalldClientRequest),
78
}
79

            
80
#[derive(Debug, Serialize)]
81
#[serde(untagged)]
82
#[allow(unused)]
83
pub 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)]
92
pub enum VarlinkReplyError {
93
    Rwhod(VarlinkRwhodClientError),
94
    Finger(VarlinkFingerClientError),
95
    Walld(VarlinkWalldClientError),
96
}
97

            
98
#[derive(Clone)]
99
pub 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

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

            
128
impl VarlinkRoowhoo2ClientServer {}
129

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

            
316
pub 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
}