1
mod classic_formatter;
2
mod parser;
3

            
4
use std::path::PathBuf;
5

            
6
use chrono::{DateTime, TimeDelta, Utc};
7
use serde::{Deserialize, Serialize};
8

            
9
use crate::{
10
    proto::finger_protocol::{
11
        classic_formatter::classic_format_finger_response_structured_user_entry,
12
        parser::try_parse_structured_user_entry_from_raw_finger_response,
13
    },
14
    util::duration_serde,
15
};
16

            
17
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18
pub struct FingerRequest {
19
    long: bool,
20
    name: String,
21
}
22

            
23
impl FingerRequest {
24
4
    pub fn new(long: bool, name: String) -> Self {
25
4
        Self { long, name }
26
4
    }
27

            
28
2
    pub fn to_bytes(&self) -> Vec<u8> {
29
2
        let mut result = Vec::new();
30
2
        if self.long {
31
1
            result.extend(b"/W ");
32
1
        }
33

            
34
2
        result.extend(self.name.as_bytes());
35
2
        result.extend(b"\r\n");
36

            
37
2
        result
38
2
    }
39

            
40
2
    pub fn from_bytes(bytes: &[u8]) -> Self {
41
2
        let (long, name) = if &bytes[..3] == b"/W " {
42
1
            (true, &bytes[3..])
43
        } else {
44
1
            (false, bytes)
45
        };
46

            
47
2
        let name = match name.strip_suffix(b"\r\n") {
48
2
            Some(new_name) => new_name,
49
            None => name,
50
        };
51

            
52
2
        Self::new(long, String::from_utf8_lossy(name).to_string())
53
2
    }
54
}
55

            
56
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
57
pub struct RawFingerResponse(String);
58

            
59
impl RawFingerResponse {
60
12
    pub fn new(content: String) -> Self {
61
12
        Self(content)
62
12
    }
63

            
64
10
    pub fn get_inner(&self) -> &str {
65
10
        &self.0
66
10
    }
67

            
68
    pub fn into_inner(self) -> String {
69
        self.0
70
    }
71

            
72
    pub fn is_empty(&self) -> bool {
73
        self.0.is_empty()
74
    }
75

            
76
2
    pub fn from_bytes(bytes: &[u8]) -> Self {
77
2
        if bytes.is_empty() {
78
            return Self(String::new());
79
2
        }
80

            
81
56
        fn normalize(c: u8) -> u8 {
82
56
            if c == (b'\r' | 0x80) || c == (b'\n' | 0x80) {
83
                c & 0x7f
84
            } else {
85
56
                c
86
            }
87
56
        }
88

            
89
2
        let normalized: Vec<u8> = bytes
90
2
            .iter()
91
2
            .copied()
92
2
            .map(normalize)
93
2
            .chain(std::iter::once(normalize(*bytes.last().unwrap())))
94
54
            .map_windows(|[a, b]| {
95
54
                if *a == b'\r' && *b == b'\n' {
96
3
                    None
97
                } else {
98
51
                    Some(*a)
99
                }
100
54
            })
101
2
            .flatten()
102
2
            .collect();
103

            
104
2
        let result = String::from_utf8_lossy(&normalized).to_string();
105

            
106
2
        Self(result)
107
2
    }
108

            
109
2
    pub fn to_bytes(&self) -> Vec<u8> {
110
2
        let mut out = Vec::with_capacity(self.0.len() + 2);
111

            
112
51
        for &b in self.0.as_bytes() {
113
51
            if b == b'\n' {
114
3
                out.extend_from_slice(b"\r\n");
115
48
            } else {
116
48
                out.push(b);
117
48
            }
118
        }
119

            
120
2
        if !self.0.ends_with('\n') {
121
            out.extend_from_slice(b"\r\n");
122
2
        }
123

            
124
2
        out
125
2
    }
126
}
127

            
128
impl From<String> for RawFingerResponse {
129
10
    fn from(s: String) -> Self {
130
10
        Self::new(s)
131
10
    }
132
}
133

            
134
impl From<&str> for RawFingerResponse {
135
    fn from(s: &str) -> Self {
136
        Self::new(s.to_string())
137
    }
138
}
139

            
140
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141
pub enum FingerResponseUserEntry {
142
    Structured(Box<FingerResponseStructuredUserEntry>),
143
    Raw(String),
144
}
145

            
146
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147
pub struct FingerResponseStructuredUserEntry {
148
    /// The unix username of this user, as noted in passwd
149
    pub username: String,
150

            
151
    /// The full name of this user, as noted in passwd
152
    pub full_name: String,
153

            
154
    /// The path to the home directory of this user, as noted in passwd
155
    pub home_dir: PathBuf,
156

            
157
    /// The path to the shell of this user, as noted in passwd
158
    pub shell: PathBuf,
159

            
160
    /// Office location, if available
161
    pub office: Option<String>,
162

            
163
    /// Office phone number, if available
164
    pub office_phone: Option<String>,
165

            
166
    /// Home phone number, if available
167
    pub home_phone: Option<String>,
168

            
169
    /// Whether the user has never logged in to this host
170
    pub never_logged_in: bool,
171

            
172
    /// A list of user sessions, sourced from utmp entries
173
    pub sessions: Vec<FingerResponseUserSession>,
174

            
175
    /// Contents of ~/.forward, if it exists
176
    pub forward_status: Option<String>,
177

            
178
    /// Whether the user has new or unread mail
179
    pub mail_status: Option<MailStatus>,
180

            
181
    /// Contents of ~/.pgpkey, if it exists
182
    pub pgp_key: Option<String>,
183

            
184
    /// Contents of ~/.project, if it exists
185
    pub project: Option<String>,
186

            
187
    /// Contents of ~/.plan, if it exists
188
    pub plan: Option<String>,
189
}
190

            
191
impl FingerResponseStructuredUserEntry {
192
    #[allow(clippy::too_many_arguments)]
193
11
    pub fn new(
194
11
        username: String,
195
11
        full_name: String,
196
11
        home_dir: PathBuf,
197
11
        shell: PathBuf,
198
11
        office: Option<String>,
199
11
        office_phone: Option<String>,
200
11
        home_phone: Option<String>,
201
11
        never_logged_in: bool,
202
11
        sessions: Vec<FingerResponseUserSession>,
203
11
        forward_status: Option<String>,
204
11
        mail_status: Option<MailStatus>,
205
11
        pgp_key: Option<String>,
206
11
        project: Option<String>,
207
11
        plan: Option<String>,
208
11
    ) -> Self {
209
11
        debug_assert!(
210
            !never_logged_in || sessions.is_empty(),
211
            "User cannot be marked as never logged in while having active sessions"
212
        );
213

            
214
11
        Self {
215
11
            username,
216
11
            full_name,
217
11
            home_dir,
218
11
            shell,
219
11
            office,
220
11
            office_phone,
221
11
            home_phone,
222
11
            never_logged_in,
223
11
            sessions,
224
11
            forward_status,
225
11
            mail_status,
226
11
            pgp_key,
227
11
            project,
228
11
            plan,
229
11
        }
230
11
    }
231

            
232
    /// Try parsing a [FingerResponseUserEntry] from the text format used by bsd-finger.
233
10
    pub fn try_from_raw_finger_response(
234
10
        response: &RawFingerResponse,
235
10
        username: String,
236
10
    ) -> anyhow::Result<Self> {
237
10
        try_parse_structured_user_entry_from_raw_finger_response(response, username)
238
10
    }
239

            
240
    pub fn classic_format(&self) -> String {
241
        classic_format_finger_response_structured_user_entry(self)
242
    }
243
}
244

            
245
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246
pub enum MailStatus {
247
    NoMail,
248
    NewMailReceived {
249
        #[serde(with = "chrono::serde::ts_seconds")]
250
        received_time: DateTime<Utc>,
251
        #[serde(with = "chrono::serde::ts_seconds")]
252
        unread_since: DateTime<Utc>,
253
    },
254
    MailLastRead(#[serde(with = "chrono::serde::ts_seconds")] DateTime<Utc>),
255
}
256

            
257
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258
pub struct FingerResponseUserSession {
259
    /// The tty on which this session exists
260
    pub tty: String,
261

            
262
    /// When the user logged in and created this session
263
    #[serde(with = "chrono::serde::ts_seconds")]
264
    pub login_time: DateTime<Utc>,
265

            
266
    /// The hostname or address of the machine from which the user is logged in, if available
267
    pub host: Option<String>,
268

            
269
    /// The amount of time since the use last interacted with the tty
270
    #[serde(with = "duration_serde::duration_seconds_option")]
271
    pub idle_time: Option<TimeDelta>,
272

            
273
    /// Whether this tty is writable, and thus can receive messages via `mesg(1)`
274
    pub messages_on: bool,
275
}
276

            
277
impl FingerResponseUserSession {
278
8
    pub fn new(
279
8
        tty: String,
280
8
        login_time: DateTime<Utc>,
281
8
        host: Option<String>,
282
8
        idle_time: Option<TimeDelta>,
283
8
        messages_on: bool,
284
8
    ) -> Self {
285
8
        Self {
286
8
            tty,
287
8
            login_time,
288
8
            host,
289
8
            idle_time,
290
8
            messages_on,
291
8
        }
292
8
    }
293
}
294

            
295
#[cfg(test)]
296
mod tests {
297
    use super::*;
298

            
299
    #[test]
300
1
    fn test_finger_raw_serialization_roundrip() {
301
1
        let request = FingerRequest::new(true, "alice".to_string());
302
1
        let bytes = request.to_bytes();
303
1
        let deserialized = FingerRequest::from_bytes(&bytes);
304
1
        assert_eq!(request, deserialized);
305

            
306
1
        let request2 = FingerRequest::new(false, "bob".to_string());
307
1
        let bytes2 = request2.to_bytes();
308
1
        let deserialized2 = FingerRequest::from_bytes(&bytes2);
309
1
        assert_eq!(request2, deserialized2);
310

            
311
1
        let response = RawFingerResponse::new("Hello, World!\nThis is a test.\n".to_string());
312
1
        let response_bytes = response.to_bytes();
313
1
        let deserialized_response = RawFingerResponse::from_bytes(&response_bytes);
314
1
        assert_eq!(response, deserialized_response);
315

            
316
1
        let response2 = RawFingerResponse::new("Single line response\n".to_string());
317
1
        let response_bytes2 = response2.to_bytes();
318
1
        let deserialized_response2 = RawFingerResponse::from_bytes(&response_bytes2);
319
1
        assert_eq!(response2, deserialized_response2);
320
1
    }
321
}