Use a regex to parse the remote host args

This commit is contained in:
ChristianVisintin
2021-02-26 16:56:03 +01:00
parent e948d598b0
commit e21bfbbd14
4 changed files with 219 additions and 121 deletions
+1
View File
@@ -23,6 +23,7 @@ Released on ???
- Enhancements: - Enhancements:
- Default choice for deleting file set to "NO" (way too easy to delete files by mistake) - Default choice for deleting file set to "NO" (way too easy to delete files by mistake)
- Added CLI options to set starting workind directory on both local and remote hosts - Added CLI options to set starting workind directory on both local and remote hosts
- Parse remote host now uses a Regex to gather parts (increased stability).
- Now bookmarks and recents are sorted in the UI (bookmarks are sorted by name; recents are sorted by connection datetime) - Now bookmarks and recents are sorted in the UI (bookmarks are sorted by name; recents are sorted by connection datetime)
## 0.3.2 ## 0.3.2
+12 -6
View File
@@ -53,16 +53,16 @@ TermSCP is basically a porting of WinSCP to terminal. So basically is a terminal
### Why TermSCP 🤔 ### Why TermSCP 🤔
It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me then to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there is midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp). It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there is midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp).
## Features 🎁 ## Features 🎁
- Different communication protocols - Different communication protocols support
- SFTP - SFTP
- SCP - SCP
- FTP and FTPS - FTP and FTPS
- Compatible with Windows, Linux, BSD and MacOS - Compatible with Windows, Linux, BSD and MacOS
- Practical user interface to explore and operate on the remote and on the local machine file system - Handy user interface to explore and operate on the remote and on the local machine file system
- Bookmarks and recent connections can be saved to access quickly to your favourite hosts - Bookmarks and recent connections can be saved to access quickly to your favourite hosts
- Supports text editors to view and edit text files - Supports text editors to view and edit text files
- Supports both SFTP/SCP authentication through SSH keys and username/password - Supports both SFTP/SCP authentication through SSH keys and username/password
@@ -163,7 +163,7 @@ brew install termscp
TermSCP can be started with the following options: TermSCP can be started with the following options:
`termscp [options]... [protocol://user@address:port] [local-wrkdir] [remote-wrkdir]` `termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
- `-P, --password <password>` if address is provided, password will be this argument - `-P, --password <password>` if address is provided, password will be this argument
- `-v, --version` Print version info - `-v, --version` Print version info
@@ -173,14 +173,14 @@ TermSCP can be started in two different mode, if no extra arguments is provided,
Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server. Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server.
If address argument is provided you can also provide the start working directory for both local and remote hosts. If address argument is provided you can also provide the start working directory for local host
### Address argument 🌎 ### Address argument 🌎
The address argument has the following syntax: The address argument has the following syntax:
```txt ```txt
[protocol]://[username@]<address>[:port] [protocol://][username@]<address>[:port][:wrkdir]
``` ```
Let's see some example of this particular syntax, since it's very comfortable and you'll probably going to use this instead of the other one... Let's see some example of this particular syntax, since it's very comfortable and you'll probably going to use this instead of the other one...
@@ -203,6 +203,12 @@ Let's see some example of this particular syntax, since it's very comfortable an
termscp scp://omar@192.168.1.31:4022 termscp scp://omar@192.168.1.31:4022
``` ```
- Connect using scp to 192.168.1.31, port is 4022; username is `omar`. You will start in directory `/tmp`
```sh
termscp scp://omar@192.168.1.31:4022:/tmp
```
#### How Password can be provided 🔐 #### How Password can be provided 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password. You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
+4 -7
View File
@@ -59,7 +59,7 @@ use filetransfer::FileTransferProtocol;
fn print_usage(opts: Options) { fn print_usage(opts: Options) {
let brief = String::from( let brief = String::from(
"Usage: termscp [options]... [protocol://user@address:port] [local-wrkdir] [remote-wrkdir]", "Usage: termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]",
); );
print!("{}", opts.usage(&brief)); print!("{}", opts.usage(&brief));
println!("\nPlease, report issues to <https://github.com/veeso/termscp>"); println!("\nPlease, report issues to <https://github.com/veeso/termscp>");
@@ -72,6 +72,7 @@ fn main() {
let mut port: u16 = 22; // Default port let mut port: u16 = 22; // Default port
let mut username: Option<String> = None; // Default username let mut username: Option<String> = None; // Default username
let mut password: Option<String> = None; // Default password let mut password: Option<String> = None; // Default password
let mut remote_wrkdir: Option<PathBuf> = None;
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
let mut ticks: Duration = Duration::from_millis(10); let mut ticks: Duration = Duration::from_millis(10);
//Process options //Process options
@@ -126,12 +127,13 @@ fn main() {
if let Some(remote) = extra_args.get(0) { if let Some(remote) = extra_args.get(0) {
// Parse address // Parse address
match utils::parser::parse_remote_opt(remote) { match utils::parser::parse_remote_opt(remote) {
Ok((addr, portn, proto, user)) => { Ok((addr, portn, proto, user, wrkdir)) => {
// Set params // Set params
address = Some(addr); address = Some(addr);
port = portn; port = portn;
protocol = proto; protocol = proto;
username = user; username = user;
remote_wrkdir = wrkdir;
} }
Err(err) => { Err(err) => {
eprintln!("Bad address option: {}", err); eprintln!("Bad address option: {}", err);
@@ -149,11 +151,6 @@ fn main() {
std::process::exit(255); std::process::exit(255);
} }
} }
// Remote directory
let remote_wrkdir: Option<PathBuf> = match extra_args.get(2) {
Some(p) => Some(PathBuf::from(p)),
None => None,
};
// Get working directory // Get working directory
let wrkdir: PathBuf = match env::current_dir() { let wrkdir: PathBuf = match env::current_dir() {
Ok(dir) => dir, Ok(dir) => dir,
+202 -108
View File
@@ -25,29 +25,47 @@
// Dependencies // Dependencies
extern crate chrono; extern crate chrono;
extern crate regex;
extern crate whoami; extern crate whoami;
// Locals // Locals
use crate::filetransfer::FileTransferProtocol; use crate::filetransfer::FileTransferProtocol;
#[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
#[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::environment; use crate::system::environment;
// Ext // Ext
use chrono::format::ParseError; use chrono::format::ParseError;
use chrono::prelude::*; use chrono::prelude::*;
use regex::Regex;
use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
// Regex
lazy_static! {
/**
* Regex matches:
* - group 1: Some(protocol) | None
* - group 2: Some(user) | None
* - group 3: Address
* - group 4: Some(port) | None
* - group 5: Some(path) | None
*/
static ref REMOTE_OPT_REGEX: Regex = Regex::new(r"(?:([a-z]+)://)?(?:([^@]+)@)?(?:([^:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?::([^:]+))?").ok().unwrap();
}
/// ### parse_remote_opt /// ### parse_remote_opt
/// ///
/// Parse remote option string. Returns in case of success a tuple made of (address, port, protocol, username) /// Parse remote option string. Returns in case of success a tuple made of (address, port, protocol, username, wrkdir)
/// For ssh if username is not provided, current user will be used. /// For ssh if username is not provided, current user will be used.
/// In case of error, message is returned /// In case of error, message is returned
/// If port is missing default port will be used for each protocol /// If port is missing default port will be used for each protocol
/// SFTP => 22 /// SFTP => 22
/// FTP => 21 /// FTP => 21
/// The option string has the following syntax /// The option string has the following syntax
/// [protocol]://[username]@{address}:[port] /// [protocol://][username@]{address}[:port][:path]
/// The only argument which is mandatory is address /// The only argument which is mandatory is address
/// NOTE: possible strings /// NOTE: possible strings
/// - 172.26.104.1 /// - 172.26.104.1
@@ -59,12 +77,18 @@ use std::time::{Duration, SystemTime};
/// ///
pub fn parse_remote_opt( pub fn parse_remote_opt(
remote: &str, remote: &str,
) -> Result<(String, u16, FileTransferProtocol, Option<String>), String> { ) -> Result<
let mut wrkstr: String = remote.to_string(); (
let address: String; String,
let mut port: u16 = 22; u16,
let mut username: Option<String> = None; FileTransferProtocol,
Option<String>,
Option<PathBuf>,
),
String,
> {
// Set protocol to default protocol // Set protocol to default protocol
#[cfg(not(test))] // NOTE: don't use configuration during tests
let mut protocol: FileTransferProtocol = match environment::init_config_dir() { let mut protocol: FileTransferProtocol = match environment::init_config_dir() {
Ok(p) => match p { Ok(p) => match p {
Some(p) => { Some(p) => {
@@ -79,71 +103,59 @@ pub fn parse_remote_opt(
}, },
Err(_) => FileTransferProtocol::Sftp, Err(_) => FileTransferProtocol::Sftp,
}; };
// Split string by '://' #[cfg(test)] // NOTE: during test set protocol just to Sftp
let tokens: Vec<&str> = wrkstr.split("://").collect(); let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
// If length is > 1, then token[0] is protocol // Match against regex
match tokens.len() { match REMOTE_OPT_REGEX.captures(remote) {
1 => {} Some(groups) => {
2 => { // Match protocol
// Parse protocol let mut port: u16 = 22;
let (m_protocol, m_port) = match FileTransferProtocol::from_str(tokens[0]) { if let Some(group) = groups.get(1) {
Ok(proto) => match proto { // Set protocol from group
FileTransferProtocol::Ftp(_) => (proto, 21), let (m_protocol, m_port) = match FileTransferProtocol::from_str(group.as_str()) {
FileTransferProtocol::Scp => (proto, 22), Ok(proto) => match proto {
FileTransferProtocol::Sftp => (proto, 22), FileTransferProtocol::Ftp(_) => (proto, 21),
FileTransferProtocol::Scp => (proto, 22),
FileTransferProtocol::Sftp => (proto, 22),
},
Err(_) => return Err(format!("Unknown protocol \"{}\"", group.as_str())),
};
// NOTE: tuple destructuring assignment is not supported yet :(
protocol = m_protocol;
port = m_port;
}
// Match user
let username: Option<String> = match groups.get(2) {
Some(group) => Some(group.as_str().to_string()),
None => match protocol {
// If group is empty, set to current user
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
Some(whoami::username())
}
_ => None,
}, },
Err(_) => return Err(format!("Unknown protocol '{}'", tokens[0])),
}; };
protocol = m_protocol; // Get address
port = m_port; let address: String = match groups.get(3) {
wrkstr = String::from(tokens[1]); // Wrkstr becomes tokens[1] Some(group) => group.as_str().to_string(),
} None => return Err(String::from("Missing address")),
_ => return Err(String::from("Bad syntax")), // Too many tokens...
}
// Set username to default if sftp or scp
if matches!(
protocol,
FileTransferProtocol::Sftp | FileTransferProtocol::Scp
) {
// Set username to current username
username = Some(whoami::username());
}
// Split wrkstring by '@'
let tokens: Vec<&str> = wrkstr.split('@').collect();
match tokens.len() {
1 => {}
2 => {
// Username is first token
username = Some(String::from(tokens[0]));
// Update wrkstr
wrkstr = String::from(tokens[1]);
}
_ => return Err(String::from("Bad syntax")), // Too many tokens...
}
// Split wrkstring by ':'
let tokens: Vec<&str> = wrkstr.split(':').collect();
match tokens.len() {
1 => {
// Address is wrkstr
address = wrkstr.clone();
}
2 => {
// Address is first token
address = String::from(tokens[0]);
// Port is second str
port = match tokens[1].parse::<u16>() {
Ok(val) => val,
Err(_) => {
return Err(format!(
"Port must be a number in range [0-65535], but is '{}'",
tokens[1]
))
}
}; };
// Get port
if let Some(group) = groups.get(4) {
port = match group.as_str().parse::<u16>() {
Ok(p) => p,
Err(err) => return Err(format!("Bad port \"{}\": {}", group.as_str(), err)),
};
}
// Get workdir
let wrkdir: Option<PathBuf> = match groups.get(5) {
Some(group) => Some(PathBuf::from(group.as_str())),
None => None,
};
Ok((address, port, protocol, username, wrkdir))
} }
_ => return Err(String::from("Bad syntax")), // Too many tokens... None => Err(String::from("Bad remote host syntax!")),
} }
Ok((address, port, protocol, username))
} }
/// ### parse_lstime /// ### parse_lstime
@@ -205,90 +217,172 @@ mod tests {
#[test] #[test]
fn test_utils_parse_remote_opt() { fn test_utils_parse_remote_opt() {
// Base case // Base case
let result: (String, u16, FileTransferProtocol, Option<String>) = let result: (
parse_remote_opt(&String::from("172.26.104.1")) String,
.ok() u16,
.unwrap(); FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1")); assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 22); assert_eq!(result.1, 22);
assert_eq!(result.2, FileTransferProtocol::Sftp); assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some()); assert!(result.3.is_some());
// User case // User case
let result: (String, u16, FileTransferProtocol, Option<String>) = let result: (
parse_remote_opt(&String::from("root@172.26.104.1")) String,
.ok() u16,
.unwrap(); FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("root@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1")); assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 22); assert_eq!(result.1, 22);
assert_eq!(result.2, FileTransferProtocol::Sftp); assert_eq!(result.2, FileTransferProtocol::Sftp);
assert_eq!(result.3.unwrap(), String::from("root")); assert_eq!(result.3.unwrap(), String::from("root"));
assert!(result.4.is_none());
// User + port // User + port
let result: (String, u16, FileTransferProtocol, Option<String>) = let result: (
parse_remote_opt(&String::from("root@172.26.104.1:8022")) String,
.ok() u16,
.unwrap(); FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1")); assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 8022); assert_eq!(result.1, 8022);
assert_eq!(result.2, FileTransferProtocol::Sftp); assert_eq!(result.2, FileTransferProtocol::Sftp);
assert_eq!(result.3.unwrap(), String::from("root")); assert_eq!(result.3.unwrap(), String::from("root"));
assert!(result.4.is_none());
// Port only // Port only
let result: (String, u16, FileTransferProtocol, Option<String>) = let result: (
parse_remote_opt(&String::from("172.26.104.1:4022")) String,
.ok() u16,
.unwrap(); FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("172.26.104.1:4022"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1")); assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 4022); assert_eq!(result.1, 4022);
assert_eq!(result.2, FileTransferProtocol::Sftp); assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some()); assert!(result.3.is_some());
assert!(result.4.is_none());
// Protocol // Protocol
let result: (String, u16, FileTransferProtocol, Option<String>) = let result: (
parse_remote_opt(&String::from("ftp://172.26.104.1")) String,
.ok() u16,
.unwrap(); FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("ftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1")); assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 21); // Fallback to ftp default assert_eq!(result.1, 21); // Fallback to ftp default
assert_eq!(result.2, FileTransferProtocol::Ftp(false)); assert_eq!(result.2, FileTransferProtocol::Ftp(false));
assert!(result.3.is_none()); // Doesn't fall back assert!(result.3.is_none()); // Doesn't fall back
// Protocol assert!(result.4.is_none());
let result: (String, u16, FileTransferProtocol, Option<String>) = // Protocol
parse_remote_opt(&String::from("sftp://172.26.104.1")) let result: (
.ok() String,
.unwrap(); u16,
FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("sftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1")); assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 22); // Fallback to sftp default assert_eq!(result.1, 22); // Fallback to sftp default
assert_eq!(result.2, FileTransferProtocol::Sftp); assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some()); // Doesn't fall back assert!(result.3.is_some()); // Doesn't fall back
let result: (String, u16, FileTransferProtocol, Option<String>) = assert!(result.4.is_none());
parse_remote_opt(&String::from("scp://172.26.104.1")) let result: (
.ok() String,
.unwrap(); u16,
FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("scp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1")); assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 22); // Fallback to scp default assert_eq!(result.1, 22); // Fallback to scp default
assert_eq!(result.2, FileTransferProtocol::Scp); assert_eq!(result.2, FileTransferProtocol::Scp);
assert!(result.3.is_some()); // Doesn't fall back assert!(result.3.is_some()); // Doesn't fall back
// Protocol + user assert!(result.4.is_none());
let result: (String, u16, FileTransferProtocol, Option<String>) = // Protocol + user
parse_remote_opt(&String::from("ftps://anon@172.26.104.1")) let result: (
.ok() String,
.unwrap(); u16,
FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1")); assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 21); // Fallback to ftp default assert_eq!(result.1, 21); // Fallback to ftp default
assert_eq!(result.2, FileTransferProtocol::Ftp(true)); assert_eq!(result.2, FileTransferProtocol::Ftp(true));
assert_eq!(result.3.unwrap(), String::from("anon")); assert_eq!(result.3.unwrap(), String::from("anon"));
assert!(result.4.is_none());
// Path
let result: (
String,
u16,
FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 8022);
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert_eq!(result.3.unwrap(), String::from("root"));
assert_eq!(result.4.unwrap(), PathBuf::from("/var"));
// Port only
let result: (
String,
u16,
FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("172.26.104.1:home"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 22);
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some());
assert_eq!(result.4.unwrap(), PathBuf::from("home"));
// All together now // All together now
let result: (String, u16, FileTransferProtocol, Option<String>) = let result: (
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021")) String,
.ok() u16,
.unwrap(); FileTransferProtocol,
Option<String>,
Option<PathBuf>,
) = parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1")); assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 8021); // Fallback to ftp default assert_eq!(result.1, 8021); // Fallback to ftp default
assert_eq!(result.2, FileTransferProtocol::Ftp(false)); assert_eq!(result.2, FileTransferProtocol::Ftp(false));
assert_eq!(result.3.unwrap(), String::from("anon")); assert_eq!(result.3.unwrap(), String::from("anon"));
assert_eq!(result.4.unwrap(), PathBuf::from("/tmp"));
// bad syntax // bad syntax
assert!(parse_remote_opt(&String::from("://172.26.104.1")).is_err()); // Missing protocol
assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err()); // Bad protocol assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err()); // Bad protocol
assert!(parse_remote_opt(&String::from("172.26.104.1:abc")).is_err()); // Bad port assert!(parse_remote_opt(&String::from("omar://172.26.104.1:650000")).is_err());
// Bad port
} }
#[test] #[test]