Skip to main content

digit/
protocol.rs

1use std::io::{self, Read, Write};
2use std::net::{TcpStream, ToSocketAddrs};
3use std::time::Duration;
4
5use crate::query::Query;
6
7/// Errors that can occur during a finger protocol exchange.
8#[derive(Debug, thiserror::Error)]
9pub enum FingerError {
10    /// Failed to resolve the hostname.
11    #[error("could not resolve host '{host}': {source}")]
12    DnsResolution {
13        host: String,
14        #[source]
15        source: io::Error,
16    },
17
18    /// Failed to connect to the remote host.
19    #[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    /// Connection timed out.
28    #[error("connection to {host}:{port} timed out")]
29    Timeout { host: String, port: u16 },
30
31    /// Failed to send the query.
32    #[error("failed to send query: {source}")]
33    SendFailed {
34        #[source]
35        source: io::Error,
36    },
37
38    /// Failed to read the response.
39    #[error("failed to read response: {source}")]
40    ReadFailed {
41        #[source]
42        source: io::Error,
43    },
44}
45
46/// Build the wire-format query string to send over the TCP connection.
47///
48/// Per RFC 1288:
49/// - Verbose queries prepend `/W ` (with trailing space).
50/// - Forwarding appends `@host1@host2...` for all hosts except the last
51///   (the last host is the connection target, not part of the query string).
52/// - The query is terminated with `\r\n`.
53pub fn build_query_string(query: &Query) -> String {
54    let mut result = String::new();
55
56    // Verbose prefix per RFC 1288.
57    if query.long {
58        result.push_str("/W ");
59    }
60
61    // User portion.
62    if let Some(ref user) = query.user {
63        result.push_str(user);
64    }
65
66    // Forwarding: include all hosts except the last (the connection target).
67    // These become @host1@host2... in the query string.
68    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
79/// Execute a finger query over TCP.
80///
81/// Connects to the target host, sends the query string, reads the full
82/// response until the server closes the connection, and returns the
83/// response as a string. Invalid UTF-8 bytes are replaced with U+FFFD.
84pub 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    // Resolve hostname to socket addresses.
89    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    // Connect with timeout.
102    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    // Set read/write timeouts on the connected socket.
118    stream.set_read_timeout(Some(timeout)).ok();
119    stream.set_write_timeout(Some(timeout)).ok();
120
121    // Send the query.
122    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    // Read the full response.
128    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}