ssh_agent_switcher/
find.rs1use log::{debug, info, trace};
27use std::io::{ErrorKind, Result};
28use std::os::unix::fs::MetadataExt;
29use std::path::Path;
30use std::{fs, path::PathBuf};
31use tokio::net::UnixStream;
32
33#[macro_export]
35macro_rules! error {
36 ( $kind:expr, $text:expr ) => {
37 std::io::Error::new($kind, $text)
38 };
39
40 ( $kind:expr, $fmt:literal $(, $args:expr)+ ) => {
41 std::io::Error::new($kind, format!($fmt $(, $args)+))
42 };
43}
44
45async fn try_open(path: &Path) -> Result<UnixStream> {
47 let name = path.file_name().expect(
48 "The path comes from joining a directory to one of its entries, so it must have a name",
49 );
50 let name = match name.to_str() {
51 Some(name) => name,
52 None => return Err(error!(ErrorKind::InvalidInput, "Invalid socket path")),
53 };
54
55 let is_pre_openssh_10_1 = name.starts_with("agent.");
56 let is_openssh_10_1 = name.contains(".sshd.");
57 if !is_pre_openssh_10_1 && !is_openssh_10_1 {
58 return Err(error!(
59 ErrorKind::InvalidInput,
60 "Socket name in does not start with 'agent.' or does not contain '.sshd.'"
61 ));
62 }
63
64 let metadata =
65 fs::metadata(path).map_err(|e| error!(e.kind(), "Failed to get metadata: {}", e))?;
66
67 if (metadata.mode() & libc::S_IFSOCK as u32) == 0 {
68 return Err(error!(ErrorKind::InvalidInput, "Path is not a socket"));
69 }
70
71 let socket = UnixStream::connect(&path)
72 .await
73 .map_err(|e| error!(e.kind(), "Cannot connect to socket: {}", e))?;
74
75 Ok(socket)
76}
77
78async fn find_in_subdir(dir: &Path) -> Option<UnixStream> {
84 let entries = match fs::read_dir(dir) {
85 Ok(entries) => entries,
86 Err(e) => {
87 debug!("Failed to read directory entries in {}: {}", dir.display(), e);
88 return None;
89 }
90 };
91
92 let mut candidates = vec![];
93 for entry in entries {
94 let entry = match entry {
95 Ok(entry) => entry,
96 Err(e) => {
97 debug!("Failed to read directory entry in {}: {}", dir.display(), e);
98 continue;
99 }
100 };
101
102 let candidate = entry.path();
103 candidates.push(candidate);
104 }
105
106 candidates.sort();
108
109 for candidate in candidates {
110 let socket = match try_open(&candidate).await {
111 Ok(socket) => socket,
112 Err(e) => {
113 trace!("Ignoring candidate socket {}: {}", candidate.display(), e);
114 continue;
115 }
116 };
117
118 info!("Successfully opened socket at {}", candidate.display());
119 return Some(socket);
120 }
121
122 debug!("No socket in directory {}", dir.display());
123 None
124}
125
126async fn try_shared_subdir(dir: &Path, uid: libc::uid_t) -> Result<UnixStream> {
130 let name = dir.file_name().expect(
137 "The candidate path comes from joining a directory to one of its entries, so it must have a name");
138 let name = match name.to_str() {
139 Some(name) => name,
140 None => return Err(error!(ErrorKind::InvalidInput, "Invalid file name")),
141 };
142
143 if !name.starts_with("ssh-") {
144 return Err(error!(ErrorKind::InvalidInput, "Basename does not start with 'ssh-'"));
145 }
146
147 let metadata = fs::metadata(dir).map_err(|e| error!(e.kind(), "Stat failed: {}", e))?;
148
149 if metadata.uid() != uid {
150 return Err(error!(
151 ErrorKind::InvalidInput,
152 "{} is owned by {}, not the current user {}",
153 dir.display(),
154 metadata.uid(),
155 uid
156 ));
157 }
158
159 match find_in_subdir(dir).await {
160 Some(socket) => Ok(socket),
161 None => Err(error!(ErrorKind::NotFound, "No socket in subdirectory")),
162 }
163}
164
165async fn find_in_shared_dir(dir: &Path, our_uid: libc::uid_t) -> Option<UnixStream> {
172 let entries = match fs::read_dir(dir) {
173 Ok(entries) => entries,
174 Err(e) => {
175 debug!("Failed to read directory entries in {}: {}", dir.display(), e);
176 return None;
177 }
178 };
179
180 let mut subdirs = vec![];
181 for entry in entries {
182 let entry = match entry {
183 Ok(entry) => entry,
184 Err(e) => {
185 debug!("Failed to read directory entry in {}: {}", dir.display(), e);
186 continue;
187 }
188 };
189 let path = entry.path();
190
191 match entry.file_type() {
192 Ok(file_type) if file_type.is_dir() => (),
193 Ok(_file_type) => {
194 trace!("Ignoring {}: not a directory", path.display());
195 continue;
196 }
197 Err(e) => {
198 trace!("Ignoring {}: {}", path.display(), e);
199 continue;
200 }
201 };
202
203 subdirs.push(path);
204 }
205
206 subdirs.sort();
208
209 for subdir in subdirs {
210 let socket = match try_shared_subdir(&subdir, our_uid).await {
211 Ok(socket) => socket,
212 Err(e) => {
213 trace!("Ignoring {}: {}", subdir.display(), e);
214 continue;
215 }
216 };
217
218 return Some(socket);
219 }
220
221 debug!("No socket in directory: {}", dir.display());
222 None
223}
224
225pub(super) async fn find_socket(
231 dirs: &[PathBuf],
232 home: Option<&Path>,
233 uid: libc::uid_t,
234) -> Option<UnixStream> {
235 for dir in dirs {
236 if let Some(home) = home
237 && dir.starts_with(home) {
238 debug!("Looking for an agent socket in {} with HOME naming scheme", dir.display());
239 if let Some(socket) = find_in_subdir(dir).await {
240 return Some(socket);
241 }
242 }
243
244 debug!("Looking for an agent socket in {} subdirs", dir.display());
245 if let Some(socket) = find_in_shared_dir(dir, uid).await {
246 return Some(socket);
247 }
248 }
249
250 None
251}