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::{
9
            DEFAULT_CLIENT_SERVER_SOCKET_PATH, VarlinkFingerClientError, VarlinkFingerClientProxy,
10
        },
11
    },
12
    version,
13
};
14

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

            
44
    /// Forces finger to use IPv6 addresses only.
45
    #[arg(long, short = '6', conflicts_with = "ipv4")]
46
    ipv6: bool,
47

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

            
64
    /// When used in conjunction with the -s option, the name of the remote host
65
    /// is displayed instead of the office location and office phone.
66
    #[arg(long, short = 'H', requires = "short", conflicts_with = "office")]
67
    host: bool,
68

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

            
82
    /// This option restricts the gecos output to only the users' real name.
83
    /// It also has the side-effect of restricting the output of the remote host
84
    /// when used in conjunction with the -H option.
85
    #[arg(long, short, requires = "short")]
86
    gecos: bool,
87

            
88
    /// Disable all use of the user accounting database.
89
    #[arg(short = 'k')]
90
    no_acct: bool,
91

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

            
115
    /// Prevent the -l option of finger from displaying the contents of
116
    /// the .forward, .plan, .project and .pubkey files.
117
    #[arg(long, short, requires = "long")]
118
    prevent_files: bool,
119

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

            
126
    /// Output in JSON format
127
    #[arg(long, short)]
128
    json: bool,
129

            
130
    /// When fingering remote users, don't try to parse the content before displaying it,
131
    /// but instead just print the bytes as they are received from the remote.
132
    ///
133
    /// Note that this option makes it impossible to represent remote users as JSON.
134
    #[arg(long, short, conflicts_with = "json")]
135
    raw: bool,
136

            
137
    /// Generate shell completion scripts for the specified shell
138
    /// and print them to stdout.
139
    #[arg(long, value_enum, hide = true)]
140
    completions: Option<Shell>,
141

            
142
    users: Option<Vec<String>>,
143
}
144

            
145
fn determine_request_info(args: &Args) -> FingerRequestInfo {
146
    let is_long = if args.long {
147
        true
148
    } else if args.short {
149
        false
150
    } else {
151
        args.users.is_some()
152
    };
153

            
154
    if is_long {
155
        FingerRequestInfo::Long {
156
            prevent_files: args.prevent_files,
157
        }
158
    } else {
159
        debug_assert!(
160
            !args.host || !args.office,
161
            "Host and office options cannot both be enabled for short output format"
162
        );
163
        if args.host {
164
            FingerRequestInfo::ShortHost {
165
                restrict_gecos: args.gecos,
166
            }
167
        } else {
168
            FingerRequestInfo::ShortOffice {
169
                restrict_gecos: args.gecos,
170
            }
171
        }
172
    }
173
}
174

            
175
#[tokio::main]
176
async fn main() -> anyhow::Result<()> {
177
    let args = Args::parse();
178

            
179
    if let Some(shell) = args.completions {
180
        generate(shell, &mut Args::command(), "rwho", &mut std::io::stdout());
181
        return Ok(());
182
    }
183

            
184
    let mut conn = zlink::tokio::unix::connect(DEFAULT_CLIENT_SERVER_SOCKET_PATH)
185
        .await
186
        .expect("Failed to connect to fingerd server");
187

            
188
    let request_info = determine_request_info(&args);
189
    let request_networking = match (args.ipv4, args.ipv6) {
190
        (true, false) => FingerRequestNetworking::IPv4Only,
191
        (false, true) => FingerRequestNetworking::IPv6Only,
192
        _ => FingerRequestNetworking::Any,
193
    };
194

            
195
    let reply = conn
196
        .finger(
197
            args.users,
198
            !args.no_name_match,
199
            request_info,
200
            request_networking,
201
            args.no_acct,
202
            args.raw,
203
        )
204
        .await
205
        .context("Failed to send finger request")?
206
        .map_err(|e| match e {
207
            VarlinkFingerClientError::Disabled => {
208
                anyhow::anyhow!("The fingerd service is disabled on the server")
209
            }
210
            VarlinkFingerClientError::TimedOut => {
211
                anyhow::anyhow!("The fingerd service timed out while processing the request")
212
            }
213
            VarlinkFingerClientError::InvalidRequest => {
214
                anyhow::anyhow!("The fingerd service could not process the request, please check the logs or report the error to your system administrators")
215
            }
216
        })?;
217

            
218
    if args.json {
219
        println!("{}", serde_json::to_string_pretty(&reply).unwrap());
220
    } else {
221
        for user in reply {
222
            match user {
223
                FingerResponseUserEntry::Structured(structured) => {
224
                    println!("{}", structured.classic_format());
225
                }
226
                FingerResponseUserEntry::Raw(raw) => {
227
                    println!("{}", raw);
228
                }
229
            }
230
        }
231
    }
232

            
233
    Ok(())
234
}