1use std::fmt;
17use std::io::{self, Read, Write};
18use std::net::{Shutdown, TcpStream, ToSocketAddrs};
19use std::path::{Path, PathBuf};
20use std::sync::{Arc, Mutex};
21use std::thread;
22use std::time::Duration;
23
24use anyhow::{Context, Result, anyhow, bail};
25use russh::MethodKind;
26use russh::client::{self, AuthResult, KeyboardInteractiveAuthResponse};
27use russh::keys::agent::AgentIdentity;
28use russh::keys::agent::client::{AgentClient, AgentStream};
29use russh::keys::known_hosts::{check_known_hosts_path, learn_known_hosts_path};
30use russh::keys::{HashAlg, PrivateKey, PrivateKeyWithHashAlg, PublicKey, PublicKeyOrCertificate};
31use russh::{ChannelMsg, Disconnect};
32use tokio::sync::{mpsc, oneshot};
33
34use crate::dialer::{Entry, Protocol};
35
36pub(crate) const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
37const MAX_KBD_INTERACTIVE_ROUNDS: usize = 5;
39const MAX_PROMPTED_ATTEMPTS: usize = 3;
41const DEFAULT_KEY_FILES: [&str; 3] = ["id_ed25519", "id_ecdsa", "id_rsa"];
43const LOGIN_CANCELLED: &str = "Login cancelled";
44
45#[derive(Debug)]
47pub enum NetEvent {
48 Connected(Writer),
50 Data(Vec<u8>),
51 Notice(String),
53 Prompt(AuthPrompt),
55 Closed(Option<String>),
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum PromptKind {
62 Password,
64 Passphrase,
66 Interactive,
69}
70
71pub struct AuthPrompt {
74 pub kind: PromptKind,
75 pub title: String,
77 pub instructions: String,
79 pub prompt: String,
80 pub echo: bool,
82 reply: oneshot::Sender<Option<String>>,
83}
84
85impl AuthPrompt {
86 pub fn answer(self, answer: Option<String>) {
88 let _ = self.reply.send(answer);
89 }
90}
91
92impl fmt::Debug for AuthPrompt {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.debug_struct("AuthPrompt")
95 .field("kind", &self.kind)
96 .field("title", &self.title)
97 .field("instructions", &self.instructions)
98 .field("prompt", &self.prompt)
99 .field("echo", &self.echo)
100 .finish()
101 }
102}
103
104#[derive(Clone, Debug)]
106pub struct ConnectOptions {
107 pub cols: u16,
108 pub rows: u16,
109 pub known_hosts: PathBuf,
111 pub ssh_agent: bool,
113 pub ssh_key_dir: Option<PathBuf>,
115 pub modem: crate::modem::ModemConfig,
117}
118
119pub(crate) enum Cmd {
121 Data(Vec<u8>),
122 Close,
123}
124
125pub struct Writer(pub(crate) WriterKind);
127
128pub(crate) enum WriterKind {
129 Tcp(TcpStream),
130 Channel(mpsc::UnboundedSender<Cmd>),
132 Serial(Mutex<Box<dyn serialport::SerialPort>>, Arc<std::sync::atomic::AtomicBool>),
134}
135
136impl fmt::Debug for Writer {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 f.write_str(match self.0 {
139 WriterKind::Tcp(_) => "Writer(TCP)",
140 WriterKind::Channel(_) => "Writer(channel)",
141 WriterKind::Serial(..) => "Writer(modem)",
142 })
143 }
144}
145
146impl Writer {
147 pub fn write_all(&mut self, data: &[u8]) -> io::Result<()> {
148 match &mut self.0 {
149 WriterKind::Tcp(s) => s.write_all(data),
150 WriterKind::Channel(tx) => tx
151 .send(Cmd::Data(data.to_vec()))
152 .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "connection closed")),
153 WriterKind::Serial(port, _) => crate::modem::write(port, data),
154 }
155 }
156
157 pub fn shutdown(&self) {
159 match &self.0 {
160 WriterKind::Tcp(s) => {
161 let _ = s.shutdown(Shutdown::Both);
162 }
163 WriterKind::Channel(tx) => {
164 let _ = tx.send(Cmd::Close);
165 }
166 WriterKind::Serial(_, stop) => stop.store(true, std::sync::atomic::Ordering::Relaxed),
167 }
168 }
169}
170
171pub fn spawn_connection<F>(entry: &Entry, opts: &ConnectOptions, sink: F)
176where
177 F: Fn(NetEvent) + Send + 'static,
178{
179 match entry.protocol {
180 Protocol::Telnet | Protocol::Raw => spawn_tcp(entry.host.clone(), entry.port, None, sink),
181 Protocol::Rlogin => spawn_tcp(entry.host.clone(), entry.port, Some(rlogin_handshake(entry)), sink),
182 Protocol::TelnetS => {
183 crate::tls::spawn_tls(entry.host.clone(), entry.port, tls_pins_path(&opts.known_hosts), sink)
184 }
185 Protocol::Ssh => {
186 let params = SshParams {
187 host: entry.host.clone(),
188 port: entry.port,
189 user: entry.username.clone(),
190 password: entry.password.clone(),
191 key_file: (!entry.key_file.trim().is_empty()).then(|| expand_home(entry.key_file.trim())),
192 agent: opts.ssh_agent,
193 key_dir: opts.ssh_key_dir.clone(),
194 term: if entry.term_type.is_empty() { "ansi".into() } else { entry.term_type.clone() },
195 cols: opts.cols,
196 rows: opts.rows,
197 known_hosts: opts.known_hosts.clone(),
198 };
199 spawn_ssh(params, sink);
200 }
201 Protocol::Modem => crate::modem::spawn_modem(entry.host.clone(), opts.modem.clone(), sink),
202 }
203}
204
205pub fn tls_pins_path(known_hosts: &Path) -> PathBuf {
207 known_hosts.with_file_name("tls_known_hosts")
208}
209
210pub fn rlogin_handshake(entry: &Entry) -> Vec<u8> {
215 let term = if entry.term_type.is_empty() { "ansi-bbs".to_string() } else { entry.term_type.to_ascii_lowercase() };
216 let mut out = vec![0];
217 for field in [entry.password.as_str(), entry.username.as_str(), &format!("{term}/115200")] {
218 out.extend_from_slice(field.as_bytes());
219 out.push(0);
220 }
221 out
222}
223
224fn spawn_tcp<F>(host: String, port: u16, handshake: Option<Vec<u8>>, sink: F)
227where
228 F: Fn(NetEvent) + Send + 'static,
229{
230 thread::spawn(move || {
231 let mut stream = match connect_tcp(&host, port) {
232 Ok(s) => s,
233 Err(e) => return sink(NetEvent::Closed(Some(format!("{e:#}")))),
234 };
235 let mut expect_ack = false;
236 if let Some(h) = &handshake {
237 if let Err(e) = stream.write_all(h) {
238 return sink(NetEvent::Closed(Some(format!("sending the RLogin login: {e}"))));
239 }
240 expect_ack = true;
241 }
242 match stream.try_clone() {
243 Ok(w) => sink(NetEvent::Connected(Writer(WriterKind::Tcp(w)))),
244 Err(e) => return sink(NetEvent::Closed(Some(e.to_string()))),
245 }
246 let mut buf = vec![0u8; 16 * 1024];
247 loop {
248 match stream.read(&mut buf) {
249 Ok(0) => return sink(NetEvent::Closed(None)),
250 Ok(n) => {
251 let mut data = &buf[..n];
252 if expect_ack {
253 expect_ack = false;
254 data = data.strip_prefix(&[0]).unwrap_or(data);
255 }
256 if !data.is_empty() {
257 sink(NetEvent::Data(data.to_vec()));
258 }
259 }
260 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
261 Err(e) => return sink(NetEvent::Closed(Some(e.to_string()))),
262 }
263 }
264 });
265}
266
267fn connect_tcp(host: &str, port: u16) -> Result<TcpStream> {
268 let addrs: Vec<_> = (host, port).to_socket_addrs().with_context(|| format!("resolving {host}"))?.collect();
269 let mut last_err = None;
270 for addr in addrs {
271 match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {
272 Ok(s) => {
273 let _ = s.set_nodelay(true);
274 return Ok(s);
275 }
276 Err(e) => last_err = Some(e),
277 }
278 }
279 match last_err {
280 Some(e) => Err(e).with_context(|| format!("connecting to {host}:{port}")),
281 None => bail!("{host} has no addresses"),
282 }
283}
284
285struct SshParams {
286 host: String,
287 port: u16,
288 user: String,
289 password: String,
290 key_file: Option<PathBuf>,
291 agent: bool,
292 key_dir: Option<PathBuf>,
293 term: String,
294 cols: u16,
295 rows: u16,
296 known_hosts: PathBuf,
297}
298
299#[derive(Clone)]
302pub(crate) struct Emitter(Arc<dyn Fn(NetEvent) + Send + Sync>);
303
304impl Emitter {
305 pub(crate) fn new<F: Fn(NetEvent) + Send + 'static>(sink: F) -> Emitter {
307 let sink = Mutex::new(sink);
308 Emitter(Arc::new(move |ev| (sink.lock().unwrap_or_else(|e| e.into_inner()))(ev)))
309 }
310
311 pub(crate) fn emit(&self, ev: NetEvent) {
312 (self.0)(ev)
313 }
314}
315
316fn spawn_ssh<F>(params: SshParams, sink: F)
317where
318 F: Fn(NetEvent) + Send + 'static,
319{
320 thread::spawn(move || {
321 let emitter = Emitter::new(sink);
322 let rt = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
323 Ok(rt) => rt,
324 Err(e) => return emitter.emit(NetEvent::Closed(Some(format!("starting SSH runtime: {e}")))),
325 };
326 let result = rt.block_on(run_ssh(params, emitter.clone()));
327 emitter.emit(NetEvent::Closed(result.err().map(|e| format!("{e:#}"))));
328 });
329}
330
331struct HostKeyCheck {
332 host: String,
333 port: u16,
334 known_hosts: PathBuf,
335 emitter: Emitter,
336 problem: Arc<Mutex<Option<String>>>,
338}
339
340impl client::Handler for HostKeyCheck {
341 type Error = russh::Error;
342
343 async fn check_server_key(&mut self, key: &PublicKeyOrCertificate) -> Result<bool, Self::Error> {
344 let key = match key {
345 PublicKeyOrCertificate::PublicKey { key, .. } => key.clone(),
346 PublicKeyOrCertificate::Certificate(cert) => PublicKey::from(cert.public_key().clone()),
347 };
348 match verify_host_key(&self.host, self.port, &key, &self.known_hosts) {
349 Ok(notice) => {
350 if let Some(n) = notice {
351 self.emitter.emit(NetEvent::Notice(n));
352 }
353 Ok(true)
354 }
355 Err(msg) => {
356 *self.problem.lock().unwrap() = Some(msg);
357 Ok(false)
358 }
359 }
360 }
361}
362
363pub fn verify_host_key(host: &str, port: u16, key: &PublicKey, known_hosts: &Path) -> Result<Option<String>, String> {
366 let fingerprint = key.fingerprint(HashAlg::Sha256);
367 match check_known_hosts_path(host, port, key, known_hosts) {
368 Ok(true) => Ok(None),
369 Ok(false) => {
370 learn_known_hosts_path(host, port, key, known_hosts)
371 .map_err(|e| format!("saving host key to {}: {e}", known_hosts.display()))?;
372 Ok(Some(format!("Trusted new SSH host key for {host}: {fingerprint}")))
373 }
374 Err(russh::keys::Error::KeyChanged { line }) => Err(format!(
375 "The SSH host key for {host} has CHANGED (now {fingerprint}). Someone may be intercepting the \
376 connection. If the BBS really changed its key, delete line {line} of {}.",
377 known_hosts.display()
378 )),
379 Err(e) => Err(format!("reading {}: {e}", known_hosts.display())),
380 }
381}
382
383async fn run_ssh(p: SshParams, emitter: Emitter) -> Result<()> {
384 if p.user.is_empty() {
385 bail!("SSH needs a user name: edit the entry and fill in \"User name\"");
386 }
387 let config = Arc::new(client::Config { keepalive_interval: Some(Duration::from_secs(30)), ..Default::default() });
388 let problem = Arc::new(Mutex::new(None));
389 let handler = HostKeyCheck {
390 host: p.host.clone(),
391 port: p.port,
392 known_hosts: p.known_hosts.clone(),
393 emitter: emitter.clone(),
394 problem: problem.clone(),
395 };
396 let connect = client::connect(config, (p.host.as_str(), p.port), handler);
397 let mut session = match tokio::time::timeout(CONNECT_TIMEOUT, connect).await {
398 Err(_) => bail!("connecting to {}:{}: timed out", p.host, p.port),
399 Ok(Err(e)) => {
400 if let Some(msg) = problem.lock().unwrap().take() {
401 bail!(msg);
402 }
403 return Err(anyhow!(e)).with_context(|| format!("connecting to {}:{}", p.host, p.port));
404 }
405 Ok(Ok(s)) => s,
406 };
407
408 Login { session: &mut session, p: &p, emitter: &emitter }.run().await?;
409
410 let channel = session.channel_open_session().await.context("opening SSH channel")?;
411 channel
412 .request_pty(false, &p.term, p.cols.into(), p.rows.into(), 0, 0, &[])
413 .await
414 .context("requesting a terminal")?;
415 channel.request_shell(false).await.context("starting the shell")?;
416
417 let (tx, mut rx) = mpsc::unbounded_channel();
418 emitter.emit(NetEvent::Connected(Writer(WriterKind::Channel(tx))));
419
420 let (mut read, write) = channel.split();
421 loop {
422 tokio::select! {
423 msg = read.wait() => match msg {
424 Some(ChannelMsg::Data { data }) => emitter.emit(NetEvent::Data(data.to_vec())),
425 Some(ChannelMsg::ExtendedData { data, .. }) => emitter.emit(NetEvent::Data(data.to_vec())),
426 Some(ChannelMsg::Eof | ChannelMsg::Close) | None => break,
427 Some(_) => {}
428 },
429 cmd = rx.recv() => match cmd {
430 Some(Cmd::Data(d)) => write.data(&d[..]).await.context("sending data")?,
431 Some(Cmd::Close) | None => break,
432 },
433 }
434 }
435 let _ = session.disconnect(Disconnect::ByApplication, "", "en").await;
436 Ok(())
437}
438
439fn expand_home(p: &str) -> PathBuf {
441 match (p.strip_prefix("~/").or_else(|| p.strip_prefix("~\\")), std::env::home_dir()) {
442 (Some(rest), Some(home)) => home.join(rest),
443 _ => PathBuf::from(p),
444 }
445}
446
447enum Attempt {
449 Success,
450 Failed(Vec<MethodKind>),
452}
453
454impl From<AuthResult> for Attempt {
455 fn from(r: AuthResult) -> Attempt {
456 match r {
457 AuthResult::Success => Attempt::Success,
458 AuthResult::Failure { remaining_methods, .. } => Attempt::Failed(remaining_methods.to_vec()),
459 }
460 }
461}
462
463struct Login<'a, H: client::Handler> {
465 session: &'a mut client::Handle<H>,
466 p: &'a SshParams,
467 emitter: &'a Emitter,
468}
469
470impl<H: client::Handler> Login<'_, H> {
471 async fn run(&mut self) -> Result<()> {
475 let user = self.p.user.clone();
476 let mut methods = match self.session.authenticate_none(&user).await?.into() {
477 Attempt::Success => return Ok(()),
478 Attempt::Failed(m) => m,
479 };
480 if methods.contains(&MethodKind::PublicKey) {
481 match self.public_keys().await? {
482 Some(Attempt::Success) => return Ok(()),
483 Some(Attempt::Failed(m)) => methods = m,
484 None => {}
485 }
486 }
487 let mut asked = false;
491 if methods.contains(&MethodKind::KeyboardInteractive) {
492 let (attempt, a) = self.keyboard_interactive().await?;
493 asked = a;
494 match attempt {
495 Attempt::Success => return Ok(()),
496 Attempt::Failed(m) => methods = m,
497 }
498 }
499 if !asked && methods.contains(&MethodKind::Password) {
500 match self.password().await? {
501 Attempt::Success => return Ok(()),
502 Attempt::Failed(m) => methods = m,
503 }
504 }
505 let offered: Vec<&str> = methods.iter().map(<&str>::from).collect();
506 log::debug!("SSH login refused; server still offers {offered:?}");
507 bail!("SSH login as \"{user}\" was refused: check the user name and password")
508 }
509
510 async fn ask(&self, kind: PromptKind, instructions: String, prompt: String, echo: bool) -> Option<String> {
511 let (reply, answer) = oneshot::channel();
512 let title = format!("SSH login to {}", self.p.host);
513 self.emitter.emit(NetEvent::Prompt(AuthPrompt { kind, title, instructions, prompt, echo, reply }));
514 answer.await.ok().flatten()
515 }
516
517 async fn ask_password(&self, retry: bool) -> Result<String> {
518 let why = if retry { "That password was not accepted." } else { "" };
519 let prompt = format!("Password for {}:", self.p.user);
520 self.ask(PromptKind::Password, why.into(), prompt, false).await.context(LOGIN_CANCELLED)
521 }
522
523 async fn password(&mut self) -> Result<Attempt> {
526 let mut stored = (!self.p.password.is_empty()).then(|| self.p.password.clone());
527 let (mut retry, mut asked) = (false, 0);
528 let mut last = Attempt::Failed(vec![MethodKind::Password]);
529 while asked < MAX_PROMPTED_ATTEMPTS {
530 let pw = match stored.take() {
531 Some(pw) => pw,
532 None => {
533 asked += 1;
534 self.ask_password(retry).await?
535 }
536 };
537 last = self.session.authenticate_password(&self.p.user, pw).await?.into();
538 match &last {
539 Attempt::Failed(m) if m.contains(&MethodKind::Password) => retry = true,
540 _ => break,
541 }
542 }
543 Ok(last)
544 }
545
546 async fn keyboard_interactive(&mut self) -> Result<(Attempt, bool)> {
550 let user = self.p.user.clone();
551 let mut stored = (!self.p.password.is_empty()).then(|| self.p.password.clone());
552 let (mut retry, mut asked) = (false, false);
553 let mut last = Attempt::Failed(vec![MethodKind::KeyboardInteractive]);
554 let attempts = MAX_PROMPTED_ATTEMPTS + usize::from(stored.is_some());
556 'attempts: for _ in 0..attempts {
557 let mut resp = self.session.authenticate_keyboard_interactive_start(&user, None).await?;
558 for _ in 0..MAX_KBD_INTERACTIVE_ROUNDS {
559 let (name, instructions, prompts) = match resp {
560 KeyboardInteractiveAuthResponse::Success => return Ok((Attempt::Success, asked)),
561 KeyboardInteractiveAuthResponse::Failure { remaining_methods, .. } => {
562 last = Attempt::Failed(remaining_methods.to_vec());
563 let used_stored = stored.is_none() && !self.p.password.is_empty();
566 if !remaining_methods.contains(&MethodKind::KeyboardInteractive) || !asked && !used_stored {
567 break 'attempts;
568 }
569 retry = true;
570 continue 'attempts;
571 }
572 KeyboardInteractiveAuthResponse::InfoRequest { name, instructions, prompts } => {
573 (name, instructions, prompts)
574 }
575 };
576 let mut answers = Vec::with_capacity(prompts.len());
577 if let ([only], Some(pw)) = (prompts.as_slice(), stored.as_ref())
578 && !only.echo
579 {
580 answers.push(pw.clone());
581 stored = None;
582 } else {
583 let mut info = [name.trim(), instructions.trim()]
584 .into_iter()
585 .filter(|s| !s.is_empty())
586 .collect::<Vec<_>>()
587 .join("\n");
588 if retry && info.is_empty() {
589 info = "That answer was not accepted.".into();
590 }
591 for q in &prompts {
592 let kind = if q.echo { PromptKind::Interactive } else { PromptKind::Password };
593 let text = if q.prompt.trim().is_empty() { "Password:" } else { q.prompt.trim() };
594 asked = true;
595 let answer = self.ask(kind, info.clone(), text.into(), q.echo).await;
596 answers.push(answer.context(LOGIN_CANCELLED)?);
597 }
598 }
599 resp = self.session.authenticate_keyboard_interactive_respond(answers).await?;
600 }
601 break;
602 }
603 Ok((last, asked))
604 }
605
606 async fn public_keys(&mut self) -> Result<Option<Attempt>> {
609 let rsa_hash = self.session.best_supported_rsa_hash().await.ok().flatten().flatten();
610 let mut offered: Vec<PublicKey> = Vec::new();
611 let mut last = None;
612
613 if let Some(path) = self.p.key_file.clone()
614 && let Some(key) = self.load_key(&path, true).await?
615 {
616 offered.push(key.public_key().clone());
617 let r = self.offer_key(key, rsa_hash).await?;
618 if matches!(r, Attempt::Success) {
619 return Ok(Some(r));
620 }
621 last = Some(r);
622 }
623
624 if self.p.agent
625 && let Some(mut agent) = connect_agent().await
626 {
627 let identities = agent.request_identities().await.unwrap_or_else(|e| {
628 log::debug!("listing SSH agent keys: {e}");
629 Vec::new()
630 });
631 for id in identities {
632 let AgentIdentity::PublicKey { key, .. } = id else { continue };
633 if offered.contains(&key) {
634 continue;
635 }
636 offered.push(key.clone());
637 let hash = if key.algorithm().is_rsa() { rsa_hash } else { None };
638 match self.session.authenticate_publickey_with(&self.p.user, key, hash, &mut agent).await {
639 Ok(r) => match Attempt::from(r) {
640 Attempt::Success => return Ok(Some(Attempt::Success)),
641 failed => last = Some(failed),
642 },
643 Err(e) => log::debug!("SSH agent signing failed: {e}"),
644 }
645 }
646 }
647
648 if let Some(dir) = self.p.key_dir.clone() {
649 for name in DEFAULT_KEY_FILES {
650 let path = dir.join(name);
651 if Some(&path) == self.p.key_file.as_ref() || !path.exists() {
652 continue;
653 }
654 let Some(key) = self.load_key(&path, false).await? else { continue };
658 if offered.contains(key.public_key()) {
659 continue;
660 }
661 offered.push(key.public_key().clone());
662 match self.offer_key(key, rsa_hash).await? {
663 Attempt::Success => return Ok(Some(Attempt::Success)),
664 failed => last = Some(failed),
665 }
666 }
667 }
668 Ok(last)
669 }
670
671 async fn offer_key(&mut self, key: PrivateKey, rsa_hash: Option<HashAlg>) -> Result<Attempt> {
672 let key = PrivateKeyWithHashAlg::new(Arc::new(key), rsa_hash);
673 Ok(self.session.authenticate_publickey(&self.p.user, key).await?.into())
674 }
675
676 async fn load_key(&self, path: &Path, ask: bool) -> Result<Option<PrivateKey>> {
680 let shown = path.display().to_string();
681 let mut why = String::new();
682 let mut passphrase: Option<String> = None;
683 for _ in 0..=MAX_PROMPTED_ATTEMPTS {
684 match russh::keys::load_secret_key(path, passphrase.as_deref()) {
685 Ok(key) => return Ok(Some(key)),
686 Err(e) if !ask => {
687 log::debug!("skipping SSH key {shown}: {e}");
688 return Ok(None);
689 }
690 Err(russh::keys::Error::KeyIsEncrypted) => {}
691 Err(russh::keys::Error::SshKey(_)) if passphrase.is_some() => {
693 why = "That passphrase was not accepted.".into();
694 }
695 Err(e) => {
696 self.emitter.emit(NetEvent::Notice(format!("Cannot use SSH key {shown}: {e}")));
697 return Ok(None);
698 }
699 }
700 let prompt = format!("Passphrase for {shown}:");
701 match self.ask(PromptKind::Passphrase, why.clone(), prompt, false).await {
702 Some(p) => passphrase = Some(p),
703 None => return Ok(None),
705 }
706 }
707 Ok(None)
708 }
709}
710
711async fn connect_agent() -> Option<AgentClient<Box<dyn AgentStream + Send + Unpin>>> {
713 #[cfg(unix)]
714 {
715 match AgentClient::connect_env().await {
716 Ok(a) => Some(a.dynamic()),
717 Err(e) => {
718 log::debug!("no SSH agent: {e}");
719 None
720 }
721 }
722 }
723 #[cfg(windows)]
724 {
725 if let Ok(a) = AgentClient::connect_named_pipe(r"\\.\pipe\openssh-ssh-agent").await {
727 return Some(a.dynamic());
728 }
729 match AgentClient::connect_pageant().await {
730 Ok(a) => Some(a.dynamic()),
731 Err(e) => {
732 log::debug!("no SSH agent: {e}");
733 None
734 }
735 }
736 }
737 #[cfg(not(any(unix, windows)))]
738 None
739}