1
use anyhow::Context;
2
use clap::{CommandFactory, Parser, builder::ArgPredicate};
3
use clap_complete::{Shell, generate};
4
use roowho2_lib::{
5
    proto::finger_protocol::FingerResponseUserEntry,
6
    server::{
7
        fingerd::{FingerRequestInfo, FingerRequestNetworking},
8
        varlink_api::VarlinkFingerClientProxy,
9
    },
10
};
11

            
12
/// User information lookup program
13
///
14
/// The `finger` utility displays information about the system users.
15
///
16
///
17
/// If no options are specified, finger defaults to the -l style output if operands are provided, otherwise to the -s style.
18
/// Note that some fields may be missing, in either format, if information is not available for them.
19
///
20
/// If no arguments are specified, finger will print an entry for each user currently logged into the system.
21
///
22
/// Finger may be used to look up users on a remote machine.
23
/// The format is to specify a user as “user@host”, or “@host”,
24
/// where the default output format for the former is the -l style,
25
/// and the default output format for the latter is the -s style.
26
/// The -l option is the only option that may be passed to a remote machine.
27
///
28
/// If standard output is a socket, finger will emit a carriage return (^M) before every linefeed (^J).
29
/// This is for processing remote finger requests when invoked by the daemon.
30
#[derive(Debug, Parser)]
31
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
32
pub struct Args {
33
    /// Forces finger to use IPv4 addresses only.
34
    #[arg(long, short = '4', conflicts_with = "ipv6")]
35
    ipv4: bool,
36

            
37
    /// Forces finger to use IPv6 addresses only.
38
    #[arg(long, short = '6', conflicts_with = "ipv4")]
39
    ipv6: bool,
40

            
41
    /// Display the user's login name, real name, terminal name and write status
42
    /// (as a ``*'' before the terminal name if write permission is denied),
43
    /// idle time, login time, and either office location and office phone number,
44
    /// or the remote host. If -o is given, the office location and office phone number
45
    /// is printed (the default). If -h is given, the remote host is printed instead.
46
    ///
47
    /// Idle time is in minutes if it is a single integer, hours and minutes if a ``:''
48
    /// is present, or days if a ``d'' is present. If it is an "*", the login time indicates
49
    /// the time of last login. Login time is displayed as the day name if less than 6 days,
50
    /// else month, day; hours and minutes, unless more than six months ago, in which case the year
51
    /// is displayed rather than the hours and minutes.
52
    ///
53
    /// Unknown devices as well as nonexistent idle and login times are displayed as single asterisks.
54
    #[arg(long, short, conflicts_with = "long")]
55
    short: bool,
56

            
57
    /// When used in conjunction with the -s option, the name of the remote host
58
    /// is displayed instead of the office location and office phone.
59
    #[arg(long, short = 'H', requires = "short", conflicts_with = "office")]
60
    host: bool,
61

            
62
    /// When used in conjunction with the -s option, the office location and
63
    /// office phone information is displayed instead of the name of the remote host.
64
    // TODO: this is default true, should be false when host is true
65
    #[arg(
66
        long,
67
        short,
68
        requires = "short",
69
        conflicts_with = "host",
70
        default_value = "true",
71
        default_value_if("host", ArgPredicate::IsPresent, "false")
72
    )]
73
    office: bool,
74

            
75
    /// This option restricts the gecos output to only the users' real name.
76
    /// It also has the side-effect of restricting the output of the remote host
77
    /// when used in conjunction with the -H option.
78
    #[arg(long, short, requires = "short")]
79
    gecos: bool,
80

            
81
    /// Disable all use of the user accounting database.
82
    #[arg(short = 'k')]
83
    no_acct: bool,
84

            
85
    /// Produce a multi-line format displaying all of the information
86
    /// described for the -s option as well as the user's home directory,
87
    /// home phone number, login shell, mail status, and the contents of
88
    /// the files .forward, .plan, .project and .pubkey from the user's home directory.
89
    ///
90
    /// If idle time is at least a minute and less than a day, it is presented in the form ``hh:mm''.
91
    /// Idle times greater than a day are presented as ``d day[s]hh:mm''
92
    ///
93
    /// Phone numbers specified as eleven digits are printed as ``+N-NNN-NNN-NNNN''.
94
    /// Numbers specified as ten or seven digits are printed as the appropriate subset of that string.
95
    /// Numbers specified as five digits are printed as ``xN-NNNN''.
96
    /// Numbers specified as four digits are printed as ``xNNNN''.
97
    ///
98
    /// If write permission is denied to the device, the phrase ``(messages off)''
99
    /// is appended to the line containing the device name. One entry per user is displayed with the -l option;
100
    /// if a user is logged on multiple times, terminal information is repeated once per login.
101
    ///
102
    /// Mail status is shown as ``No Mail.'' if there is no mail at all,
103
    /// ``Mail last read DDD MMM ## HH:MM YYYY (TZ)'' if the person has looked at their mailbox since new mail arriving,
104
    /// or ``New mail received ...'', ``Unread since ...'' if they have new mail.
105
    #[arg(long, short, conflicts_with = "short")]
106
    long: bool,
107

            
108
    /// Prevent the -l option of finger from displaying the contents of
109
    /// the .forward, .plan, .project and .pubkey files.
110
    #[arg(long, short, requires = "long")]
111
    prevent_files: bool,
112

            
113
    /// Prevent matching of user names. User is usually a login name;
114
    /// however, matching will also be done on the users' real names,
115
    /// unless the -m option is supplied. All name matching performed by finger is case insensitive.
116
    #[arg(long, short = 'm')]
117
    no_name_match: bool,
118

            
119
    /// Output in JSON format
120
    #[arg(long, short)]
121
    json: bool,
122

            
123
    /// When fingering remote users, don't try to parse the content before displaying it,
124
    /// but instead just print the bytes as they are received from the remote.
125
    ///
126
    /// Note that this option makes it impossible to represent remote users as JSON.
127
    #[arg(long, short, conflicts_with = "json")]
128
    raw: bool,
129

            
130
    /// Generate shell completion scripts for the specified shell
131
    /// and print them to stdout.
132
    #[arg(long, value_enum, hide = true)]
133
    completions: Option<Shell>,
134

            
135
    users: Option<Vec<String>>,
136
}
137

            
138
fn determine_request_info(args: &Args) -> FingerRequestInfo {
139
    let is_long = if args.long {
140
        true
141
    } else if args.short {
142
        false
143
    } else {
144
        args.users.is_some()
145
    };
146

            
147
    if is_long {
148
        FingerRequestInfo::Long {
149
            prevent_files: args.prevent_files,
150
        }
151
    } else {
152
        debug_assert!(
153
            !args.host || !args.office,
154
            "Host and office options cannot both be enabled for short output format"
155
        );
156
        if args.host {
157
            FingerRequestInfo::ShortHost {
158
                restrict_gecos: args.gecos,
159
            }
160
        } else {
161
            FingerRequestInfo::ShortOffice {
162
                restrict_gecos: args.gecos,
163
            }
164
        }
165
    }
166
}
167

            
168
#[tokio::main]
169
async fn main() -> anyhow::Result<()> {
170
    let args = Args::parse();
171

            
172
    if let Some(shell) = args.completions {
173
        generate(shell, &mut Args::command(), "rwho", &mut std::io::stdout());
174
        return Ok(());
175
    }
176

            
177
    let mut conn = zlink::unix::connect("/run/roowho2/roowho2.varlink")
178
        .await
179
        .expect("Failed to connect to fingerd server");
180

            
181
    let request_info = determine_request_info(&args);
182
    let request_networking = match (args.ipv4, args.ipv6) {
183
        (true, false) => FingerRequestNetworking::IPv4Only,
184
        (false, true) => FingerRequestNetworking::IPv6Only,
185
        _ => FingerRequestNetworking::Any,
186
    };
187

            
188
    let reply = conn
189
        .finger(
190
            args.users,
191
            !args.no_name_match,
192
            request_info,
193
            request_networking,
194
            args.no_acct,
195
            args.raw,
196
        )
197
        .await
198
        .context("Failed to send finger request")?
199
        .map_err(|e| anyhow::anyhow!("Server returned an error for finger request: {:?}", e))?;
200

            
201
    if args.json {
202
        println!("{}", serde_json::to_string_pretty(&reply).unwrap());
203
    } else {
204
        for user in reply {
205
            match user {
206
                FingerResponseUserEntry::Structured(structured) => {
207
                    println!("{}", structured.classic_format());
208
                }
209
                FingerResponseUserEntry::Raw(raw) => {
210
                    println!("{}", raw);
211
                }
212
            }
213
        }
214
    }
215

            
216
    Ok(())
217
}