1use std::io::{self, Read, Write};
2use std::net::{TcpStream, ToSocketAddrs};
3use std::time::Duration;
4
5use crate::query::Query;
6
7#[derive(Debug, thiserror::Error)]
9pub enum FingerError {
10 #[error("could not resolve host '{host}': {source}")]
12 DnsResolution {
13 host: String,
14 #[source]
15 source: io::Error,
16 },
17
18 #[error("could not connect to {host}:{port}: {source}")]
20 ConnectionFailed {
21 host: String,
22 port: u16,
23 #[source]
24 source: io::Error,
25 },
26
27 #[error("connection to {host}:{port} timed out")]
29 Timeout { host: String, port: u16 },
30
31 #[error("failed to send query: {source}")]
33 SendFailed {
34 #[source]
35 source: io::Error,
36 },
37
38 #[error("failed to read response: {source}")]
40 ReadFailed {
41 #[source]
42 source: io::Error,
43 },
44}
45
46pub fn build_query_string(query: &Query) -> String {
54 let mut result = String::new();
55
56 if query.long {
58 result.push_str("/W ");
59 }
60
61 if let Some(ref user) = query.user {
63 result.push_str(user);
64 }
65
66 if query.hosts.len() > 1 {
69 for host in &query.hosts[..query.hosts.len() - 1] {
70 result.push('@');
71 result.push_str(host);
72 }
73 }
74
75 result.push_str("\r\n");
76 result
77}
78
79pub fn finger(query: &Query, timeout: Duration) -> Result<String, FingerError> {
85 let host = query.target_host();
86 let addr_str = format!("{}:{}", host, query.port);
87
88 let addr = addr_str
90 .to_socket_addrs()
91 .map_err(|e| FingerError::DnsResolution {
92 host: host.to_string(),
93 source: e,
94 })?
95 .next()
96 .ok_or_else(|| FingerError::DnsResolution {
97 host: host.to_string(),
98 source: io::Error::new(io::ErrorKind::NotFound, "no addresses found"),
99 })?;
100
101 let mut stream = TcpStream::connect_timeout(&addr, timeout).map_err(|e| {
103 if e.kind() == io::ErrorKind::TimedOut {
104 FingerError::Timeout {
105 host: host.to_string(),
106 port: query.port,
107 }
108 } else {
109 FingerError::ConnectionFailed {
110 host: host.to_string(),
111 port: query.port,
112 source: e,
113 }
114 }
115 })?;
116
117 stream.set_read_timeout(Some(timeout)).ok();
119 stream.set_write_timeout(Some(timeout)).ok();
120
121 let query_string = build_query_string(query);
123 stream
124 .write_all(query_string.as_bytes())
125 .map_err(|e| FingerError::SendFailed { source: e })?;
126
127 let mut buf = Vec::new();
129 stream
130 .read_to_end(&mut buf)
131 .map_err(|e| FingerError::ReadFailed { source: e })?;
132
133 Ok(String::from_utf8_lossy(&buf).into_owned())
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::query::Query;
140
141 #[test]
142 fn query_string_user_at_host() {
143 let q = Query::parse(Some("user@host"), false, 79);
144 assert_eq!(build_query_string(&q), "user\r\n");
145 }
146
147 #[test]
148 fn query_string_list_users() {
149 let q = Query::parse(Some("@host"), false, 79);
150 assert_eq!(build_query_string(&q), "\r\n");
151 }
152
153 #[test]
154 fn query_string_verbose_user() {
155 let q = Query::parse(Some("user@host"), true, 79);
156 assert_eq!(build_query_string(&q), "/W user\r\n");
157 }
158
159 #[test]
160 fn query_string_verbose_list() {
161 let q = Query::parse(Some("@host"), true, 79);
162 assert_eq!(build_query_string(&q), "/W \r\n");
163 }
164
165 #[test]
166 fn query_string_forwarding() {
167 let q = Query::parse(Some("user@host1@host2"), false, 79);
168 assert_eq!(build_query_string(&q), "user@host1\r\n");
169 }
170
171 #[test]
172 fn query_string_forwarding_verbose() {
173 let q = Query::parse(Some("user@host1@host2"), true, 79);
174 assert_eq!(build_query_string(&q), "/W user@host1\r\n");
175 }
176
177 #[test]
178 fn query_string_forwarding_no_user() {
179 let q = Query::parse(Some("@host1@host2"), false, 79);
180 assert_eq!(build_query_string(&q), "@host1\r\n");
181 }
182
183 #[test]
184 fn query_string_three_host_chain() {
185 let q = Query::parse(Some("user@a@b@c"), false, 79);
186 assert_eq!(build_query_string(&q), "user@a@b\r\n");
187 }
188
189 #[test]
190 fn query_string_localhost_user() {
191 let q = Query::parse(Some("user"), false, 79);
192 assert_eq!(build_query_string(&q), "user\r\n");
193 }
194
195 #[test]
196 fn query_string_localhost_list() {
197 let q = Query::parse(None, false, 79);
198 assert_eq!(build_query_string(&q), "\r\n");
199 }
200}