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