1use crate as eg;
2use eg::Editor;
3use eg::EgResult;
4use ftp::FtpStream;
5use glob;
6use std::cmp;
7use std::env;
8use std::fmt;
9use std::fs;
10use std::io::Read;
11use std::io::Write;
12use std::net::TcpStream;
13use std::net::ToSocketAddrs;
14use std::path::Path;
15use std::path::PathBuf;
16use std::time::Duration;
17use url::Url;
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum Proto {
21 Sftp,
22 Ftp,
23}
24
25impl From<Proto> for &'static str {
26 fn from(p: Proto) -> &'static str {
27 match p {
28 Proto::Sftp => "sftp",
29 Proto::Ftp => "ftp",
30 }
31 }
32}
33
34pub struct RemoteAccount {
35 host: String,
36 port: Option<u16>,
37 username: Option<String>,
38 password: Option<String>,
39 proto: Proto,
40
41 remote_path: Option<PathBuf>,
42
43 sftp_session: Option<ssh2::Sftp>,
44
45 ftp_session: Option<ftp::FtpStream>,
46
47 timeout: u32,
49
50 remote_account_id: Option<i64>,
51
52 ssh_private_key: Option<PathBuf>,
54 ssh_private_key_password: Option<String>,
55
56 try_typical_ssh_keys: bool,
57}
58
59impl cmp::PartialEq for RemoteAccount {
60 fn eq(&self, other: &RemoteAccount) -> bool {
62 self.proto == other.proto
63 && self.host == other.host
64 && self.port == other.port
65 && self.username == other.username
66 && self.password == other.password
67 && self.remote_path == other.remote_path
68 }
69}
70
71impl fmt::Display for RemoteAccount {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(
74 f,
75 "RemoteAccount host={} user={}",
76 self.host,
77 self.username.as_deref().unwrap_or("")
78 )
79 }
80}
81
82impl RemoteAccount {
83 pub fn new(host: &str) -> RemoteAccount {
84 RemoteAccount {
85 host: host.to_string(),
86 port: None,
87 username: None,
88 password: None,
89 proto: Proto::Sftp,
90 remote_path: None,
91 timeout: 0,
92 ssh_private_key: None,
93 ssh_private_key_password: None,
94 sftp_session: None,
95 ftp_session: None,
96 try_typical_ssh_keys: true,
97 remote_account_id: None,
98 }
99 }
100
101 pub fn from_edi_account(
104 editor: &mut Editor,
105 account_id: i64,
106 read_mode: bool,
107 ) -> EgResult<RemoteAccount> {
108 let edi_account = editor
109 .retrieve("acqedi", account_id)?
110 .ok_or_else(|| editor.die_event())?;
111
112 let mut account = RemoteAccount::from_url_string(edi_account["host"].str()?)?;
113
114 account.remote_account_id = Some(account_id);
115
116 account.remote_path = if read_mode {
117 edi_account["in_dir"].as_str().map(PathBuf::from)
118 } else {
119 edi_account["path"].as_str().map(PathBuf::from)
120 };
121
122 if let Some(username) = edi_account["username"].as_str() {
123 account.set_username(username);
124 }
125
126 if let Some(password) = edi_account["password"].as_str() {
127 account.set_password(password);
128 }
129
130 Ok(account)
131 }
132
133 pub fn from_url_string(url_str: &str) -> EgResult<RemoteAccount> {
137 let url = Url::parse(url_str).map_err(|e| format!("Cannot parse URL: {url_str} : {e}"))?;
138
139 let hostname = url.host_str().ok_or("URL has no host")?;
140 let mut account = RemoteAccount::new(hostname);
141
142 account.proto = match url.scheme() {
143 "sftp" => Proto::Sftp,
144 "ftp" | "" => Proto::Ftp,
145 _ => return Err(format!("Unsupported protocol: '{url_str}'").into()),
146 };
147
148 if !url.username().is_empty() {
149 account.set_username(url.username());
150 }
151
152 if !url.path().is_empty() {
153 account.remote_path = Some(PathBuf::from(url.path()));
154 }
155
156 Ok(account)
157 }
158
159 pub fn host(&self) -> &str {
160 &self.host
161 }
162
163 pub fn port(&self) -> Option<u16> {
164 self.port
165 }
166
167 pub fn proto(&self) -> Proto {
168 self.proto
169 }
170
171 pub fn username(&self) -> Option<&str> {
172 self.username.as_deref()
173 }
174
175 pub fn password(&self) -> Option<&str> {
176 self.password.as_deref()
177 }
178
179 pub fn remote_account_id(&self) -> Option<i64> {
180 self.remote_account_id
181 }
182
183 pub fn remote_path(&self) -> Option<&Path> {
184 self.remote_path.as_deref()
185 }
186
187 pub fn set_remote_path(&mut self, remote_path: &Path) {
188 self.remote_path = Some(remote_path.to_path_buf());
189 }
190
191 pub fn set_username(&mut self, username: &str) {
192 self.username = Some(username.to_string());
193 }
194
195 pub fn set_password(&mut self, password: &str) {
196 self.password = Some(password.to_string());
197 }
198
199 pub fn set_ssh_private_key(&mut self, keyfile: &Path) {
200 self.ssh_private_key = Some(keyfile.to_path_buf());
201 }
202
203 pub fn set_ssh_private_key_password(&mut self, keypass: &str) {
204 self.ssh_private_key_password = Some(keypass.to_string());
205 }
206
207 pub fn set_timeout(&mut self, timeout: u32) {
209 self.timeout = timeout;
210 }
211
212 pub fn connect(&mut self) -> EgResult<()> {
214 log::debug!("{self} connect()");
215
216 match self.proto {
217 Proto::Sftp => self.connect_sftp(),
218 Proto::Ftp => self.connect_ftp(),
219 }
220 }
221
222 pub fn ls(&mut self) -> EgResult<Vec<PathBuf>> {
224 log::debug!(
225 "{self} ls() {:?}",
226 self.remote_path.as_ref().map(|p| p.display())
227 );
228
229 self.check_connected()?;
230
231 match self.proto {
232 Proto::Sftp => self.ls_sftp(),
233 Proto::Ftp => self.ls_ftp(),
234 }
235 }
236
237 pub fn get(&mut self, remote_path: &Path, local_path: &Path) -> EgResult<fs::File> {
240 log::debug!(
241 "{self} get() {} => {}",
242 remote_path.display(),
243 local_path.display()
244 );
245
246 self.check_connected()?;
247
248 match self.proto {
249 Proto::Sftp => self.get_sftp(remote_path, local_path),
250 Proto::Ftp => self.get_ftp(remote_path, local_path),
251 }
252 }
253
254 pub fn check_connected(&self) -> EgResult<()> {
258 match self.proto {
259 Proto::Sftp => {
260 if self.sftp_session.is_none() {
261 return Err(format!("{self} is not connected to SFTP").into());
262 }
263 }
264 Proto::Ftp => {
265 if self.ftp_session.is_none() {
266 return Err(format!("{self} is not connected to FTP").into());
267 }
268 }
269 }
270
271 Ok(())
272 }
273
274 fn get_sftp(&self, remote_path: &Path, local_path: &Path) -> EgResult<fs::File> {
277 let remote_filename = remote_path.display();
278
279 let mut remote_file = self
280 .sftp_session
281 .as_ref()
282 .unwrap()
283 .open(remote_path)
284 .map_err(|e| format!("Cannot open remote path {remote_filename} {e}"))?;
285
286 let mut bytes: Vec<u8> = Vec::new();
287 remote_file
288 .read_to_end(&mut bytes)
289 .map_err(|e| format!("Cannot read remote file: {remote_filename} {e}"))?;
290
291 self.write_local_file(local_path, &bytes)
292 }
293
294 fn get_ftp(&mut self, remote_path: &Path, local_path: &Path) -> EgResult<fs::File> {
297 let remote_filename = remote_path.display().to_string();
298
299 let cursor = self
300 .ftp_session
301 .as_mut()
302 .unwrap()
303 .simple_retr(&remote_filename)
304 .map_err(|e| format!("Cannot open remote file {remote_filename} {e}"))?;
305
306 let bytes = cursor.into_inner();
307
308 self.write_local_file(local_path, &bytes)
309 }
310
311 fn write_local_file(&self, local_path: &Path, bytes: &[u8]) -> EgResult<fs::File> {
312 let local_filename = local_path.display();
313
314 let mut local_file = fs::File::create(local_path)
315 .map_err(|e| format!("Cannot create local file {local_filename} {e}"))?;
316
317 local_file
318 .write_all(bytes)
319 .map_err(|e| format!("Cannot write to local file: {local_filename} {e}"))?;
320
321 Ok(local_file)
322 }
323
324 fn ls_sftp(&self) -> EgResult<Vec<PathBuf>> {
329 let (remote_path, maybe_glob) = self.remote_path_and_glob()?;
330
331 log::info!("{self} listing directory {}", remote_path.display());
332
333 let contents = self
334 .sftp_session
335 .as_ref()
336 .unwrap()
337 .readdir(&remote_path)
338 .map_err(|e| {
339 format!(
340 "{self} cannot list directory {} : {e}",
341 remote_path.display()
342 )
343 })?;
344
345 let mut paths = Vec::new();
346
347 for (path, _) in contents {
348 if let Some(pattern) = maybe_glob.as_ref() {
349 if let Some(Some(name)) = path.file_name().map(|n| n.to_str()) {
350 if pattern.matches(name) {
351 paths.push(path);
352 }
353 } else {
354 log::warn!("{self} skipping non-stringifiable path: {path:?}");
355 }
356 } else {
357 paths.push(path);
358 }
359 }
360
361 Ok(paths)
362 }
363
364 fn ls_ftp(&mut self) -> EgResult<Vec<PathBuf>> {
369 let (mut remote_path, maybe_glob) = self.remote_path_and_glob()?;
370
371 let remote_filename = remote_path.display().to_string();
372
373 log::info!("{self} listing directory {remote_filename}");
374
375 let contents = self
376 .ftp_session
377 .as_mut()
378 .unwrap()
379 .nlst(Some(&remote_filename))
380 .map_err(|e| format!("{self} cannot list directory {remote_filename} : {e}"))?;
381
382 let mut paths = Vec::new();
383
384 for file_name in contents {
389 remote_path.push(&file_name);
390
391 if let Some(pattern) = maybe_glob.as_ref() {
392 if pattern.matches(&file_name) {
393 paths.push(remote_path.clone());
394 }
395 } else {
396 paths.push(remote_path.clone());
397 }
398
399 remote_path.pop(); }
401
402 Ok(paths)
403 }
404
405 fn connect_sftp(&mut self) -> EgResult<()> {
406 let port = self.port.unwrap_or(22);
407 let host = self.host.as_str();
408
409 let username = self
410 .username
411 .as_deref()
412 .ok_or("SFTP connection requires a username")?;
413
414 let tcp_result = if self.timeout > 0 {
415 let sock_addr = format!("{host}:{port}")
416 .to_socket_addrs()
417 .map_err(|e| format!("Cannot resolve host: {host} : {e}"))?
418 .next()
419 .ok_or_else(|| format!("Cannot resolve host: {host}"))?;
420
421 TcpStream::connect_timeout(&sock_addr, Duration::from_secs(self.timeout.into()))
422 } else {
423 TcpStream::connect((host, port))
424 };
425
426 let tcp_stream = tcp_result.map_err(|e| format!("Cannot connect to {host} : {e}"))?;
427
428 let mut sess = ssh2::Session::new()
429 .map_err(|e| format!("Cannot create SFTP session to {host} : {e}"))?;
430
431 if self.timeout > 0 {
432 sess.set_timeout(self.timeout * 1000); }
434
435 sess.set_tcp_stream(tcp_stream);
436
437 sess.handshake()
438 .map_err(|e| format!("SFTP handshake failed to {host} : {e}"))?;
439
440 if let Some(password) = self.password.as_ref() {
441 sess.userauth_password(username, password)
442 .map_err(|e| format!("Password auth failed for host {host}: {e}"))?;
443 } else {
444 self.sftp_key_auth(&mut sess, username)?;
445 }
446
447 if !sess.authenticated() {
448 return Err(format!("SFTP connection failed to authenticate with {host}").into());
449 }
450
451 let sftp = sess
452 .sftp()
453 .map_err(|e| format!("Cannot upgrade to SFTP connection for host {host}: {e}"))?;
454
455 self.sftp_session = Some(sftp);
456
457 log::info!("Successfully connected to SFTP at {host}");
458
459 Ok(())
460 }
461
462 fn connect_ftp(&mut self) -> EgResult<()> {
463 let port = self.port.unwrap_or(21);
464 let host = self.host.as_str();
465
466 let username = self
467 .username
468 .as_deref()
469 .ok_or("FTP connection requires a username")?;
470
471 let password = self
472 .password
473 .as_deref()
474 .ok_or("FTP connection requires a password")?;
475
476 let mut stream = FtpStream::connect(format!("{host}:{port}"))
477 .map_err(|e| format!("Cannot connect to host: {host}:{port} : {e}"))?;
478
479 if self.timeout > 0 {
480 stream
481 .get_ref()
482 .set_read_timeout(Some(Duration::from_secs(self.timeout.into())))
483 .map_err(|e| format!("Cannot set read timeout {} : {e}", self.timeout))?;
484 }
485
486 stream
487 .login(username, password)
488 .map_err(|e| format!("Login failed at host {host} for {username} : {e}"))?;
489
490 self.ftp_session = Some(stream);
491
492 Ok(())
493 }
494
495 fn sftp_key_auth(&self, sess: &mut ssh2::Session, username: &str) -> EgResult<()> {
498 let mut key_files = Vec::new();
499
500 if let Some(keyfile) = self.ssh_private_key.as_ref() {
501 key_files.push(keyfile.display().to_string());
502 }
503
504 if self.try_typical_ssh_keys {
505 if let Some(home) = env::vars().find(|(k, _)| k == "HOME").map(|(_, v)| v) {
506 let mut path_buf = PathBuf::new();
507
508 path_buf.push(home);
509 path_buf.push(".ssh");
510 path_buf.push("id_rsa");
511
512 key_files.push(path_buf.as_os_str().to_string_lossy().to_string());
513
514 path_buf.pop();
515 path_buf.push("dsa_rsa");
516
517 key_files.push(path_buf.as_os_str().to_string_lossy().to_string());
518 }
519 }
520
521 for key_file in key_files {
522 log::debug!("Trying key file {key_file}");
523
524 let result = sess.userauth_pubkey_file(
525 username,
526 None,
527 Path::new(&key_file),
528 self.ssh_private_key_password.as_deref(),
529 );
530
531 if result.is_ok() {
532 return Ok(());
533 }
534 }
535
536 Err("No suitable SSH keys found".into())
537 }
538
539 fn remote_path_and_glob(&self) -> EgResult<(PathBuf, Option<glob::Pattern>)> {
544 let remote_path = self.remote_path.as_deref().unwrap_or(Path::new("/"));
545
546 let filename = match remote_path.file_name().map(|f| f.to_str()) {
548 Some(Some(f)) => f,
549 _ => return Ok((remote_path.to_path_buf(), None)),
550 };
551
552 if !filename.contains('*') {
554 return Ok((remote_path.to_path_buf(), None));
555 }
556
557 let glob_pattern = glob::Pattern::new(filename)
560 .map_err(|e| format!("Invalid glob pattern: {filename} : {e}"))?;
561
562 let mut path_buf = PathBuf::new();
563
564 for part in remote_path.iter() {
566 path_buf.push(part);
567 }
568
569 path_buf.pop();
571
572 Ok((path_buf, Some(glob_pattern)))
573 }
574}