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

View File

@@ -23,6 +23,7 @@ Released on ???
- Enhancements:
- 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
- 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)
## 0.3.2

View File

@@ -53,16 +53,16 @@ TermSCP is basically a porting of WinSCP to terminal. So basically is a terminal
### 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 🎁
- Different communication protocols
- Different communication protocols support
- SFTP
- SCP
- FTP and FTPS
- 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
- Supports text editors to view and edit text files
- 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 [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
- `-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.
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 🌎
The address argument has the following syntax:
```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...
@@ -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
```
- 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 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.

View File

@@ -59,7 +59,7 @@ use filetransfer::FileTransferProtocol;
fn print_usage(opts: Options) {
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));
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 username: Option<String> = None; // Default username
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 ticks: Duration = Duration::from_millis(10);
//Process options
@@ -126,12 +127,13 @@ fn main() {
if let Some(remote) = extra_args.get(0) {
// Parse address
match utils::parser::parse_remote_opt(remote) {
Ok((addr, portn, proto, user)) => {
Ok((addr, portn, proto, user, wrkdir)) => {
// Set params
address = Some(addr);
port = portn;
protocol = proto;
username = user;
remote_wrkdir = wrkdir;
}
Err(err) => {
eprintln!("Bad address option: {}", err);
@@ -149,11 +151,6 @@ fn main() {
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
let wrkdir: PathBuf = match env::current_dir() {
Ok(dir) => dir,

View File

@@ -25,29 +25,47 @@
// Dependencies
extern crate chrono;
extern crate regex;
extern crate whoami;
// Locals
use crate::filetransfer::FileTransferProtocol;
#[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::config_client::ConfigClient;
#[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::environment;
// Ext
use chrono::format::ParseError;
use chrono::prelude::*;
use regex::Regex;
use std::path::PathBuf;
use std::str::FromStr;
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 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.
/// In case of error, message is returned
/// If port is missing default port will be used for each protocol
/// SFTP => 22
/// FTP => 21
/// The option string has the following syntax
/// [protocol]://[username]@{address}:[port]
/// [protocol://][username@]{address}[:port][:path]
/// The only argument which is mandatory is address
/// NOTE: possible strings
/// - 172.26.104.1
@@ -59,12 +77,18 @@ use std::time::{Duration, SystemTime};
///
pub fn parse_remote_opt(
remote: &str,
) -> Result<(String, u16, FileTransferProtocol, Option<String>), String> {
let mut wrkstr: String = remote.to_string();
let address: String;
let mut port: u16 = 22;
let mut username: Option<String> = None;
) -> Result<
(
String,
u16,
FileTransferProtocol,
Option<String>,
Option<PathBuf>,
),
String,
> {
// Set protocol to default protocol
#[cfg(not(test))] // NOTE: don't use configuration during tests
let mut protocol: FileTransferProtocol = match environment::init_config_dir() {
Ok(p) => match p {
Some(p) => {
@@ -79,71 +103,59 @@ pub fn parse_remote_opt(
},
Err(_) => FileTransferProtocol::Sftp,
};
// Split string by '://'
let tokens: Vec<&str> = wrkstr.split("://").collect();
// If length is > 1, then token[0] is protocol
match tokens.len() {
1 => {}
2 => {
// Parse protocol
let (m_protocol, m_port) = match FileTransferProtocol::from_str(tokens[0]) {
#[cfg(test)] // NOTE: during test set protocol just to Sftp
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
// Match against regex
match REMOTE_OPT_REGEX.captures(remote) {
Some(groups) => {
// Match protocol
let mut port: u16 = 22;
if let Some(group) = groups.get(1) {
// Set protocol from group
let (m_protocol, m_port) = match FileTransferProtocol::from_str(group.as_str()) {
Ok(proto) => match proto {
FileTransferProtocol::Ftp(_) => (proto, 21),
FileTransferProtocol::Scp => (proto, 22),
FileTransferProtocol::Sftp => (proto, 22),
},
Err(_) => return Err(format!("Unknown protocol '{}'", tokens[0])),
Err(_) => return Err(format!("Unknown protocol \"{}\"", group.as_str())),
};
// NOTE: tuple destructuring assignment is not supported yet :(
protocol = m_protocol;
port = m_port;
wrkstr = String::from(tokens[1]); // Wrkstr becomes tokens[1]
}
_ => 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]
))
// 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,
},
};
// Get address
let address: String = match groups.get(3) {
Some(group) => group.as_str().to_string(),
None => return Err(String::from("Missing address")),
};
// 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)),
};
}
_ => return Err(String::from("Bad syntax")), // Too many tokens...
// 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))
}
None => Err(String::from("Bad remote host syntax!")),
}
Ok((address, port, protocol, username))
}
/// ### parse_lstime
@@ -205,8 +217,13 @@ mod tests {
#[test]
fn test_utils_parse_remote_opt() {
// Base case
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("172.26.104.1"))
let result: (
String,
u16,
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"));
@@ -214,81 +231,158 @@ mod tests {
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some());
// User case
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("root@172.26.104.1"))
let result: (
String,
u16,
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.1, 22);
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert_eq!(result.3.unwrap(), String::from("root"));
assert!(result.4.is_none());
// User + port
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("root@172.26.104.1:8022"))
let result: (
String,
u16,
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.1, 8022);
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert_eq!(result.3.unwrap(), String::from("root"));
assert!(result.4.is_none());
// Port only
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("172.26.104.1:4022"))
let result: (
String,
u16,
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.1, 4022);
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some());
assert!(result.4.is_none());
// Protocol
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("ftp://172.26.104.1"))
let result: (
String,
u16,
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.1, 21); // Fallback to ftp default
assert_eq!(result.2, FileTransferProtocol::Ftp(false));
assert!(result.3.is_none()); // Doesn't fall back
assert!(result.4.is_none());
// Protocol
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("sftp://172.26.104.1"))
let result: (
String,
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.1, 22); // Fallback to sftp default
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some()); // Doesn't fall back
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("scp://172.26.104.1"))
assert!(result.4.is_none());
let result: (
String,
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.1, 22); // Fallback to scp default
assert_eq!(result.2, FileTransferProtocol::Scp);
assert!(result.3.is_some()); // Doesn't fall back
assert!(result.4.is_none());
// Protocol + user
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
let result: (
String,
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.1, 21); // Fallback to ftp default
assert_eq!(result.2, FileTransferProtocol::Ftp(true));
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
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021"))
let result: (
String,
u16,
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.1, 8021); // Fallback to ftp default
assert_eq!(result.2, FileTransferProtocol::Ftp(false));
assert_eq!(result.3.unwrap(), String::from("anon"));
assert_eq!(result.4.unwrap(), PathBuf::from("/tmp"));
// 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("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]