1use std::io::{Read, Seek, Write};
2use std::net::{SocketAddr, TcpStream, ToSocketAddrs as _};
3use std::path::{Path, PathBuf};
4use std::str::FromStr as _;
5use std::time::{Duration, SystemTime};
6
7use remotefs::fs::stream::{ReadAndSeek, WriteAndSeek};
8use remotefs::fs::{FileType, Metadata, ReadStream, UnixPex, WriteStream};
9use remotefs::{File, RemoteError, RemoteErrorType, RemoteResult};
10use ssh2::{FileStat, OpenType, RenameFlags};
11
12use super::SshSession;
13use crate::ssh::backend::Sftp;
14use crate::ssh::config::Config;
15use crate::{SshAgentIdentity, SshOpts};
16
17pub struct LibSsh2Session {
19 session: ssh2::Session,
20}
21
22pub struct LibSsh2Sftp {
24 inner: ssh2::Sftp,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29enum Authentication {
30 RsaKey(PathBuf),
31 Password(String),
32}
33
34impl SshSession for LibSsh2Session {
35 type Sftp = LibSsh2Sftp;
36
37 fn connect(opts: &SshOpts) -> RemoteResult<Self> {
38 let ssh_config = Config::try_from(opts)?;
40 debug!("Connecting to '{}'", ssh_config.address);
42 let socket_addresses: Vec<SocketAddr> = match ssh_config.address.to_socket_addrs() {
44 Ok(s) => s.collect(),
45 Err(err) => {
46 return Err(RemoteError::new_ex(
47 RemoteErrorType::BadAddress,
48 err.to_string(),
49 ));
50 }
51 };
52 let mut stream = None;
53 for _ in 0..ssh_config.connection_attempts {
54 for socket_addr in socket_addresses.iter() {
55 trace!(
56 "Trying to connect to socket address '{}' (timeout: {}s)",
57 socket_addr,
58 ssh_config.connection_timeout.as_secs()
59 );
60 if let Ok(tcp_stream) = tcp_connect(socket_addr, ssh_config.connection_timeout) {
61 debug!("Connection established with address {socket_addr}");
62 stream = Some(tcp_stream);
63 break;
64 }
65 }
66 if stream.is_some() {
68 break;
69 }
70 }
71 let stream = match stream {
73 Some(s) => s,
74 None => {
75 error!("No suitable socket address found; connection timeout");
76 return Err(RemoteError::new_ex(
77 RemoteErrorType::ConnectionError,
78 "connection timeout",
79 ));
80 }
81 };
82 let mut session = match ssh2::Session::new() {
84 Ok(s) => s,
85 Err(err) => {
86 error!("Could not create session: {err}");
87 return Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, err));
88 }
89 };
90 session.set_tcp_stream(stream);
92 set_algo_prefs(&mut session, opts, &ssh_config)?;
94 if let Err(err) = session.handshake() {
96 error!("SSH handshake failed: {err}");
97 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
98 }
99
100 if let Some(ssh_agent_config) = &opts.ssh_agent_identity {
102 match session_auth_with_agent(&mut session, &ssh_config.username, ssh_agent_config) {
103 Ok(_) => {
104 info!("Authenticated with ssh agent");
105 return Ok(Self { session });
106 }
107 Err(err) => {
108 error!("Could not authenticate with ssh agent: {err}");
109 }
110 }
111 }
112
113 if !session.authenticated() {
115 let mut methods = vec![];
116 if let Some(rsa_key) = opts.key_storage.as_ref().and_then(|x| {
118 x.resolve(ssh_config.host.as_str(), ssh_config.username.as_str())
119 .or(x.resolve(
120 ssh_config.resolved_host.as_str(),
121 ssh_config.username.as_str(),
122 ))
123 }) {
124 methods.push(Authentication::RsaKey(rsa_key.clone()));
125 }
126 if let Some(password) = opts.password.as_ref() {
128 methods.push(Authentication::Password(password.clone()));
129 }
130
131 let mut last_err = None;
133 for auth_method in methods {
134 match session_auth(&mut session, opts, &ssh_config, auth_method) {
135 Ok(_) => {
136 info!("Authenticated successfully");
137 return Ok(Self { session });
138 }
139 Err(err) => {
140 error!("Authentication failed: {err}",);
141 last_err = Some(err);
142 }
143 }
144 }
145
146 return Err(last_err.unwrap_or_else(|| {
147 RemoteError::new_ex(
148 RemoteErrorType::AuthenticationFailed,
149 "no authentication method provided",
150 )
151 }));
152 }
153
154 Ok(Self { session })
155 }
156
157 fn disconnect(&self) -> RemoteResult<()> {
158 self.session
159 .disconnect(None, "Mandi!", None)
160 .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))
161 }
162
163 fn authenticated(&self) -> RemoteResult<bool> {
164 Ok(self.session.authenticated())
165 }
166
167 fn banner(&self) -> RemoteResult<Option<String>> {
168 Ok(self.session.banner().map(String::from))
169 }
170
171 fn cmd<S>(&mut self, cmd: S) -> RemoteResult<(u32, String)>
172 where
173 S: AsRef<str>,
174 {
175 let output = perform_shell_cmd(&mut self.session, format!("{}; echo $?", cmd.as_ref()))?;
176 if let Some(index) = output.trim().rfind('\n') {
177 trace!("Read from stdout: '{output}'");
178 let actual_output = (output[0..index + 1]).to_string();
179 trace!("Actual output '{actual_output}'");
180 trace!("Parsing return code '{}'", output[index..].trim());
181 let rc = match u32::from_str(output[index..].trim()).ok() {
182 Some(val) => val,
183 None => {
184 return Err(RemoteError::new_ex(
185 RemoteErrorType::ProtocolError,
186 "Failed to get command exit code",
187 ));
188 }
189 };
190 debug!(r#"Command output: "{actual_output}"; exit code: {rc}"#);
191 Ok((rc, actual_output))
192 } else {
193 match u32::from_str(output.trim()).ok() {
194 Some(val) => Ok((val, String::new())),
195 None => Err(RemoteError::new_ex(
196 RemoteErrorType::ProtocolError,
197 "Failed to get command exit code",
198 )),
199 }
200 }
201 }
202
203 fn scp_recv(&self, path: &Path) -> RemoteResult<Box<dyn Read + Send>> {
204 self.session.set_blocking(true);
205
206 self.session
207 .scp_recv(path)
208 .map(|(reader, _stat)| Box::new(reader) as Box<dyn Read + Send>)
209 .map_err(|err| {
210 RemoteError::new_ex(
211 RemoteErrorType::ProtocolError,
212 format!("Could not receive file over SCP: {err}"),
213 )
214 })
215 }
216
217 fn scp_send(
218 &self,
219 remote_path: &Path,
220 mode: i32,
221 size: u64,
222 times: Option<(u64, u64)>,
223 ) -> RemoteResult<Box<dyn Write + Send>> {
224 self.session.set_blocking(true);
225
226 self.session
227 .scp_send(remote_path, mode, size, times)
228 .map(|writer| Box::new(writer) as Box<dyn Write + Send>)
229 .map_err(|err| {
230 RemoteError::new_ex(
231 RemoteErrorType::ProtocolError,
232 format!("Could not send file over SCP: {err}"),
233 )
234 })
235 }
236
237 fn sftp(&self) -> RemoteResult<Self::Sftp> {
238 self.session.set_blocking(true);
239
240 Ok(LibSsh2Sftp {
241 inner: self.session.sftp().map_err(|err| {
242 RemoteError::new_ex(
243 RemoteErrorType::ProtocolError,
244 format!("Could not create SFTP session: {err}"),
245 )
246 })?,
247 })
248 }
249}
250
251struct SftpFileReader(ssh2::File);
252
253struct SftpFileWriter(ssh2::File);
254
255impl Write for SftpFileWriter {
256 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
257 self.0.write(buf)
258 }
259
260 fn flush(&mut self) -> std::io::Result<()> {
261 self.0.flush()
262 }
263}
264
265impl Seek for SftpFileWriter {
266 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
267 self.0.seek(pos)
268 }
269}
270
271impl WriteAndSeek for SftpFileWriter {}
272
273impl Read for SftpFileReader {
274 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
275 self.0.read(buf)
276 }
277}
278
279impl Seek for SftpFileReader {
280 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
281 self.0.seek(pos)
282 }
283}
284
285impl ReadAndSeek for SftpFileReader {}
286
287impl Sftp for LibSsh2Sftp {
288 fn mkdir(&self, path: &Path, mode: i32) -> RemoteResult<()> {
289 self.inner.mkdir(path, mode).map_err(|err| {
290 RemoteError::new_ex(
291 RemoteErrorType::FileCreateDenied,
292 format!(
293 "Could not create directory '{path}': {err}",
294 path = path.display()
295 ),
296 )
297 })
298 }
299
300 fn open_read(&self, path: &Path) -> RemoteResult<ReadStream> {
301 self.inner
302 .open(path)
303 .map(|file| ReadStream::from(Box::new(SftpFileReader(file)) as Box<dyn ReadAndSeek>))
304 .map_err(|err| {
305 RemoteError::new_ex(
306 RemoteErrorType::ProtocolError,
307 format!(
308 "Could not open file at '{path}': {err}",
309 path = path.display()
310 ),
311 )
312 })
313 }
314
315 fn open_write(
316 &self,
317 path: &Path,
318 flags: super::WriteMode,
319 mode: i32,
320 ) -> RemoteResult<WriteStream> {
321 let flags = match flags {
322 super::WriteMode::Append => {
323 ssh2::OpenFlags::WRITE | ssh2::OpenFlags::APPEND | ssh2::OpenFlags::CREATE
324 }
325 super::WriteMode::Truncate => {
326 ssh2::OpenFlags::WRITE | ssh2::OpenFlags::CREATE | ssh2::OpenFlags::TRUNCATE
327 }
328 };
329
330 self.inner
331 .open_mode(path, flags, mode, OpenType::File)
332 .map(|file| WriteStream::from(Box::new(SftpFileWriter(file)) as Box<dyn WriteAndSeek>))
333 .map_err(|err| {
334 RemoteError::new_ex(
335 RemoteErrorType::ProtocolError,
336 format!(
337 "Could not open file at '{path}': {err}",
338 path = path.display()
339 ),
340 )
341 })
342 }
343
344 fn readdir<T>(&self, dirname: T) -> RemoteResult<Vec<remotefs::File>>
345 where
346 T: AsRef<Path>,
347 {
348 self.inner
349 .readdir(dirname)
350 .map(|files| {
351 files
352 .into_iter()
353 .map(|(path, metadata)| self.make_fsentry(path.as_path(), &metadata))
354 .collect()
355 })
356 .map_err(|err| {
357 RemoteError::new_ex(
358 RemoteErrorType::ProtocolError,
359 format!("Could not read directory: {err}",),
360 )
361 })
362 }
363
364 fn realpath(&self, path: &Path) -> RemoteResult<PathBuf> {
365 self.inner.realpath(path).map_err(|err| {
366 RemoteError::new_ex(
367 RemoteErrorType::ProtocolError,
368 format!(
369 "Could not resolve real path for '{path}': {err}",
370 path = path.display()
371 ),
372 )
373 })
374 }
375
376 fn rename(&self, src: &Path, dest: &Path) -> RemoteResult<()> {
377 self.inner
378 .rename(src, dest, Some(RenameFlags::OVERWRITE))
379 .map_err(|err| {
380 RemoteError::new_ex(
381 RemoteErrorType::ProtocolError,
382 format!("Could not rename file '{src}': {err}", src = src.display()),
383 )
384 })
385 }
386
387 fn rmdir(&self, path: &Path) -> RemoteResult<()> {
388 self.inner.rmdir(path).map_err(|err| {
389 RemoteError::new_ex(
390 RemoteErrorType::CouldNotRemoveFile,
391 format!(
392 "Could not remove directory '{path}': {err}",
393 path = path.display()
394 ),
395 )
396 })
397 }
398
399 fn setstat(&self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
400 self.inner
401 .setstat(path, Self::metadata_to_filestat(metadata))
402 .map_err(|err| {
403 RemoteError::new_ex(
404 RemoteErrorType::ProtocolError,
405 format!(
406 "Could not set file attributes for '{path}': {err}",
407 path = path.display()
408 ),
409 )
410 })
411 }
412
413 fn stat(&self, filename: &Path) -> RemoteResult<File> {
414 self.inner
415 .stat(filename)
416 .map(|metadata| self.make_fsentry(filename, &metadata))
417 .map_err(|err| {
418 RemoteError::new_ex(
419 RemoteErrorType::ProtocolError,
420 format!(
421 "Could not get file attributes for '{filename}': {err}",
422 filename = filename.display()
423 ),
424 )
425 })
426 }
427
428 fn symlink(&self, path: &Path, target: &Path) -> RemoteResult<()> {
429 self.inner.symlink(path, target).map_err(|err| {
430 RemoteError::new_ex(
431 RemoteErrorType::FileCreateDenied,
432 format!(
433 "Could not create symlink '{path}': {err}",
434 path = path.display()
435 ),
436 )
437 })
438 }
439
440 fn unlink(&self, path: &Path) -> RemoteResult<()> {
441 self.inner.unlink(path).map_err(|err| {
442 RemoteError::new_ex(
443 RemoteErrorType::CouldNotRemoveFile,
444 format!(
445 "Could not remove file '{path}': {err}",
446 path = path.display()
447 ),
448 )
449 })
450 }
451}
452
453impl LibSsh2Sftp {
454 fn metadata_to_filestat(metadata: Metadata) -> FileStat {
455 let atime = metadata
456 .accessed
457 .and_then(|x| x.duration_since(SystemTime::UNIX_EPOCH).ok())
458 .map(|x| x.as_secs());
459 let mtime = metadata
460 .modified
461 .and_then(|x| x.duration_since(SystemTime::UNIX_EPOCH).ok())
462 .map(|x| x.as_secs());
463 FileStat {
464 size: Some(metadata.size),
465 uid: metadata.uid,
466 gid: metadata.gid,
467 perm: metadata.mode.map(u32::from),
468 atime,
469 mtime,
470 }
471 }
472
473 fn make_fsentry(&self, path: &Path, metadata: &FileStat) -> File {
474 let name = match path.file_name() {
475 None => "/".to_string(),
476 Some(name) => name.to_string_lossy().to_string(),
477 };
478 debug!("Found file {name}");
479 let uid = metadata.uid;
481 let gid = metadata.gid;
482 let mode = metadata.perm.map(UnixPex::from);
483 let size = metadata.size.unwrap_or(0);
484 let accessed = metadata.atime.map(|x| {
485 SystemTime::UNIX_EPOCH
486 .checked_add(Duration::from_secs(x))
487 .unwrap_or(SystemTime::UNIX_EPOCH)
488 });
489 let modified = metadata.mtime.map(|x| {
490 SystemTime::UNIX_EPOCH
491 .checked_add(Duration::from_secs(x))
492 .unwrap_or(SystemTime::UNIX_EPOCH)
493 });
494 let symlink = match metadata.file_type().is_symlink() {
495 false => None,
496 true => match self.inner.readlink(path) {
497 Ok(p) => Some(p),
498 Err(err) => {
499 error!(
500 "Failed to read link of {} (even it's supposed to be a symlink): {}",
501 path.display(),
502 err
503 );
504 None
505 }
506 },
507 };
508 let file_type = if symlink.is_some() {
509 FileType::Symlink
510 } else if metadata.is_dir() {
511 FileType::Directory
512 } else {
513 FileType::File
514 };
515 let entry_metadata = Metadata {
516 accessed,
517 created: None,
518 file_type,
519 gid,
520 mode,
521 modified,
522 size,
523 symlink,
524 uid,
525 };
526 trace!("Metadata for {}: {:?}", path.display(), entry_metadata);
527 File {
528 path: path.to_path_buf(),
529 metadata: entry_metadata,
530 }
531 }
532}
533
534fn perform_shell_cmd<S: AsRef<str>>(session: &mut ssh2::Session, cmd: S) -> RemoteResult<String> {
535 trace!("Running command: {}", cmd.as_ref());
537 let mut channel = match session.channel_session() {
538 Ok(ch) => ch,
539 Err(err) => {
540 return Err(RemoteError::new_ex(
541 RemoteErrorType::ProtocolError,
542 format!("Could not open channel: {err}"),
543 ));
544 }
545 };
546
547 let cmd = cmd.as_ref().replace('\'', r#"'\''"#); if let Err(err) = channel.exec(format!("sh -c '{cmd}'").as_str()) {
554 return Err(RemoteError::new_ex(
555 RemoteErrorType::ProtocolError,
556 format!("Could not execute command \"{cmd}\": {err}"),
557 ));
558 }
559 let mut output: String = String::new();
561 match channel.read_to_string(&mut output) {
562 Ok(_) => {
563 let _ = channel.wait_close();
565 trace!("Command output: {output}");
566 Ok(output)
567 }
568 Err(err) => Err(RemoteError::new_ex(
569 RemoteErrorType::ProtocolError,
570 format!("Could not read output: {err}"),
571 )),
572 }
573}
574
575fn tcp_connect(address: &SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
578 if timeout.is_zero() {
579 TcpStream::connect(address)
580 } else {
581 TcpStream::connect_timeout(address, timeout)
582 }
583}
584
585fn set_algo_prefs(
587 session: &mut ssh2::Session,
588 opts: &SshOpts,
589 config: &Config,
590) -> RemoteResult<()> {
591 let params = &config.params;
593 trace!("Configuring algorithm preferences...");
594 if let Some(compress) = params.compression {
595 trace!("compression: {compress}");
596 session.set_compress(compress);
597 }
598
599 let algos = params.kex_algorithms.algorithms().join(",");
601 trace!("Configuring KEX algorithms: {algos}");
602 if let Err(err) = session.method_pref(ssh2::MethodType::Kex, algos.as_str()) {
603 error!("Could not set KEX algorithms: {err}");
604 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
605 }
606
607 let algos = params.host_key_algorithms.algorithms().join(",");
609 trace!("Configuring HostKey algorithms: {algos}");
610 if let Err(err) = session.method_pref(ssh2::MethodType::HostKey, algos.as_str()) {
611 error!("Could not set host key algorithms: {err}");
612 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
613 }
614
615 let algos = params.ciphers.algorithms().join(",");
617 trace!("Configuring Crypt algorithms: {algos}");
618 if let Err(err) = session.method_pref(ssh2::MethodType::CryptCs, algos.as_str()) {
619 error!("Could not set crypt algorithms (client-server): {err}");
620 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
621 }
622 if let Err(err) = session.method_pref(ssh2::MethodType::CryptSc, algos.as_str()) {
623 error!("Could not set crypt algorithms (server-client): {err}");
624 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
625 }
626
627 let algos = params.mac.algorithms().join(",");
629 trace!("Configuring MAC algorithms: {algos}");
630 if let Err(err) = session.method_pref(ssh2::MethodType::MacCs, algos.as_str()) {
631 error!("Could not set MAC algorithms (client-server): {err}");
632 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
633 }
634 if let Err(err) = session.method_pref(ssh2::MethodType::MacSc, algos.as_str()) {
635 error!("Could not set MAC algorithms (server-client): {err}");
636 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
637 }
638
639 for method in opts.methods.iter() {
641 let algos = method.prefs();
642 trace!("Configuring {:?} algorithm: {}", method.method_type, algos);
643 if let Err(err) = session.method_pref(method.method_type.into(), algos.as_str()) {
644 error!("Could not set {:?} algorithms: {}", method.method_type, err);
645 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
646 }
647 }
648 Ok(())
649}
650
651fn session_auth_with_agent(
653 session: &mut ssh2::Session,
654 username: &str,
655 ssh_agent_config: &SshAgentIdentity,
656) -> RemoteResult<()> {
657 let mut agent = session
658 .agent()
659 .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?;
660
661 agent
662 .connect()
663 .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?;
664
665 agent
666 .list_identities()
667 .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?;
668
669 let mut connection_result = Err(RemoteError::new(RemoteErrorType::AuthenticationFailed));
670
671 for identity in agent
672 .identities()
673 .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?
674 {
675 if ssh_agent_config.pubkey_matches(identity.blob()) {
676 debug!("Trying to authenticate with ssh agent with key: {identity:?}");
677 } else {
678 continue;
679 }
680 match agent.userauth(username, &identity) {
681 Ok(()) => {
682 connection_result = Ok(());
683 debug!("Authenticated with ssh agent with key: {identity:?}");
684 break;
685 }
686 Err(err) => {
687 debug!("SSH agent auth failed: {err}");
688 connection_result = Err(RemoteError::new_ex(
689 RemoteErrorType::AuthenticationFailed,
690 err,
691 ));
692 }
693 }
694 }
695
696 if let Err(err) = agent.disconnect() {
697 warn!("Could not disconnect from ssh agent: {err}");
698 }
699
700 connection_result
701}
702
703fn session_auth_with_rsakey(
705 session: &mut ssh2::Session,
706 username: &str,
707 private_key: &Path,
708 password: Option<&str>,
709 identity_file: Option<&[PathBuf]>,
710) -> RemoteResult<()> {
711 debug!("Authenticating with username '{username}' and RSA key");
712 let mut keys = vec![private_key];
713 if let Some(identity_file) = identity_file {
714 let other_keys: Vec<&Path> = identity_file.iter().map(|x| x.as_path()).collect();
715 keys.extend(other_keys);
716 }
717 for key in keys.into_iter() {
719 trace!("Trying to authenticate with RSA key at '{}'", key.display());
720 match session.userauth_pubkey_file(username, None, key, password) {
721 Ok(_) => {
722 debug!("Authenticated with key at '{}'", key.display());
723 return Ok(());
724 }
725 Err(err) => {
726 error!("Authentication failed: {err}");
727 }
728 }
729 }
730 Err(RemoteError::new_ex(
731 RemoteErrorType::AuthenticationFailed,
732 "could not find any suitable RSA key to authenticate with",
733 ))
734}
735
736fn session_auth(
738 session: &mut ssh2::Session,
739 opts: &SshOpts,
740 ssh_config: &Config,
741 authentication: Authentication,
742) -> RemoteResult<()> {
743 match authentication {
744 Authentication::RsaKey(private_key) => session_auth_with_rsakey(
745 session,
746 &ssh_config.username,
747 private_key.as_path(),
748 opts.password.as_deref(),
749 ssh_config.params.identity_file.as_deref(),
750 ),
751 Authentication::Password(password) => {
752 session_auth_with_password(session, &ssh_config.username, &password)
753 }
754 }
755}
756
757fn session_auth_with_password(
759 session: &mut ssh2::Session,
760 username: &str,
761 password: &str,
762) -> RemoteResult<()> {
763 debug!("Authenticating with username '{username}' and password");
765 if let Err(err) = session.userauth_password(username, password) {
766 error!("Authentication failed: {err}");
767 Err(RemoteError::new_ex(
768 RemoteErrorType::AuthenticationFailed,
769 err,
770 ))
771 } else {
772 Ok(())
773 }
774}
775
776#[cfg(test)]
777mod test {
778
779 use ssh2_config::ParseRule;
780
781 use super::*;
782 use crate::mock::ssh as ssh_mock;
783
784 #[test]
785 fn should_connect_to_ssh_server_auth_user_password() {
786 use crate::ssh::container::OpensshServer;
787
788 let container = OpensshServer::start();
789 let port = container.port();
790
791 crate::mock::logger();
792 let config_file = ssh_mock::create_ssh_config(port);
793 let opts = SshOpts::new("sftp")
794 .config_file(config_file.path(), ParseRule::ALLOW_UNKNOWN_FIELDS)
795 .password("password");
796
797 if let Err(err) = LibSsh2Session::connect(&opts) {
798 panic!("Could not connect to server: {err}");
799 }
800 let session = LibSsh2Session::connect(&opts).unwrap();
801 assert!(session.authenticated().unwrap());
802
803 drop(container);
804 }
805
806 #[test]
807 fn should_connect_to_ssh_server_auth_key() {
808 use crate::ssh::container::OpensshServer;
809
810 let container = OpensshServer::start();
811 let port = container.port();
812
813 crate::mock::logger();
814 let config_file = ssh_mock::create_ssh_config(port);
815 let opts = SshOpts::new("sftp")
816 .config_file(config_file.path(), ParseRule::ALLOW_UNKNOWN_FIELDS)
817 .key_storage(Box::new(ssh_mock::MockSshKeyStorage::default()));
818 let session = LibSsh2Session::connect(&opts).unwrap();
819 assert!(session.authenticated().unwrap());
820 }
821
822 #[test]
823
824 fn should_perform_shell_command_on_server() {
825 crate::mock::logger();
826 let container = crate::ssh::container::OpensshServer::start();
827 let port = container.port();
828
829 let opts = SshOpts::new("127.0.0.1")
830 .port(port)
831 .username("sftp")
832 .password("password");
833 let mut session = LibSsh2Session::connect(&opts).unwrap();
834 assert!(session.authenticated().unwrap());
835 assert!(session.cmd("pwd").is_ok());
837 }
838
839 #[test]
840
841 fn should_perform_shell_command_on_server_and_return_exit_code() {
842 crate::mock::logger();
843 let container = crate::ssh::container::OpensshServer::start();
844 let port = container.port();
845
846 let opts = SshOpts::new("127.0.0.1")
847 .port(port)
848 .username("sftp")
849 .password("password");
850 let mut session = LibSsh2Session::connect(&opts).unwrap();
851 assert!(session.authenticated().unwrap());
852 assert_eq!(
854 session.cmd_at("pwd", Path::new("/tmp")).ok().unwrap(),
855 (0, String::from("/tmp\n"))
856 );
857 assert_eq!(
858 session
859 .cmd_at("pippopluto", Path::new("/tmp"))
860 .ok()
861 .unwrap()
862 .0,
863 127
864 );
865 }
866
867 #[test]
868 fn should_fail_authentication() {
869 crate::mock::logger();
870 let container = crate::ssh::container::OpensshServer::start();
871 let port = container.port();
872
873 let opts = SshOpts::new("127.0.0.1")
874 .port(port)
875 .username("sftp")
876 .password("ippopotamo");
877 assert!(LibSsh2Session::connect(&opts).is_err());
878 }
879
880 #[test]
881 fn test_filetransfer_sftp_bad_server() {
882 crate::mock::logger();
883 let opts = SshOpts::new("myverybad.verybad.server")
884 .port(10022)
885 .username("sftp")
886 .password("ippopotamo");
887 assert!(LibSsh2Session::connect(&opts).is_err());
888 }
889}