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