27 Commits

Author SHA1 Message Date
veeso
3f2be65a6c termscp 0.3.1 2021-01-18 17:56:13 +01:00
veeso
a5a745e444 0.3.1 2021-01-18 17:36:53 +01:00
Christian Visintin
7ee142314e Merge pull request #4 from Fenex/refactoring/1
Refactoring FtpFileTransfer::parse_unix_list_line
2021-01-18 14:25:24 +01:00
Christian Visintin
c412d98ec7 Merge branch '0.3.1' into refactoring/1 2021-01-18 08:07:00 +01:00
Vitaliy Busko
d7e5eacd79 Refactoring ScpFileTransfer::parse_ls_output 2021-01-18 11:31:35 +07:00
Vitaliy Busko
367fb235f6 Refactoring FtpFileTransfer::parse_unix_list_line 2021-01-18 10:51:22 +07:00
Christian Visintin
d68d63b978 Merge pull request #3 from veeso/keystorage
Keystorage
2021-01-16 18:10:18 +01:00
ChristianVisintin
23ca2baa8c Cargo clippy 2021-01-16 18:02:12 +01:00
ChristianVisintin
ac02928e69 Don't run bookmarks tests on macos 2021-01-16 17:37:03 +01:00
ChristianVisintin
cb20589b01 Macos test thread (?) 2021-01-16 17:27:07 +01:00
ChristianVisintin
1acbf89717 keyring ok 2021-01-16 17:13:41 +01:00
ChristianVisintin
08d8a3621c Keyring storage in bookmarks client (if possible) 2021-01-16 16:57:00 +01:00
ChristianVisintin
0192b86422 Check if supported (test) 2021-01-16 16:07:53 +01:00
ChristianVisintin
0e4caaecfd Keyring storage 2021-01-16 16:07:11 +01:00
ChristianVisintin
215927d432 Fixed copyright header 2021-01-16 15:37:29 +01:00
ChristianVisintin
eee08bd623 Key storage (file) 2021-01-16 15:37:19 +01:00
ChristianVisintin
76fdd9864c Fixed copyright header 2021-01-16 15:13:58 +01:00
ChristianVisintin
350443ec99 SCP file transfer: fixed possible wrong file size when sending file, due to a possible incoherent size between the file explorer and the actual file size 2021-01-16 11:49:59 +01:00
ChristianVisintin
928fc1b450 Solved index of files list no more kept after 0.3.0 (use set_abs_index instead) 2021-01-16 11:35:33 +01:00
ChristianVisintin
03e1bf53d0 Solved index of files list no more kept after 0.3.0 2021-01-16 11:16:31 +01:00
ChristianVisintin
9330025d07 Connection timeout for SFTP/SCP clients 2021-01-16 10:58:07 +01:00
ChristianVisintin
bf56a269e0 Replaced Box<dyn Iterator... with impl Iterator 2021-01-16 10:37:53 +01:00
ChristianVisintin
0393c1a850 working on 0.3.1... 2021-01-16 10:33:34 +01:00
ChristianVisintin
69ece00ae2 working on 0.3.1... 2021-01-16 10:31:24 +01:00
Christian Visintin
97656536d4 Cargo build requirements 2021-01-13 10:49:46 +01:00
Christian Visintin
94b78d85ef Merge pull request #1 from Byron/main
run `cargo diet` to reduce crate size by 85%
2021-01-11 08:11:52 +01:00
Sebastian Thiel
ba3a888d26 run cargo diet to reduce crate size by 85%
I noticed the crate is pretty big and took a quick look, here
is the outcome of slimming it down.

```
➜  termscp git:(main) cargo diet
┌───────────────────────────────────────────┬─────────────┐
│ File                                      │ Size (Byte) │
├───────────────────────────────────────────┼─────────────┤
│ codecov.yml                               │          96 │
│ .github/ISSUE_TEMPLATE/feature_request.md │         203 │
│ dist/deb.sh                               │         210 │
│ .github/workflows/macos.yml               │         319 │
│ .github/workflows/windows.yml             │         321 │
│ dist/rpm.sh                               │         336 │
│ dist/build/README.md                      │         345 │
│ .gitignore                                │         356 │
│ dist/build/x86_64/Dockerfile              │         509 │
│ dist/pkgs/arch/.SRCINFO                   │         511 │
│ .github/workflows/aur-pub.yml             │         570 │
│ dist/pkgs/arch/PKGBUILD                   │         580 │
│ .github/ISSUE_TEMPLATE/bug_report.md      │         598 │
│ dist/build/x86_64_archlinux/Dockerfile    │         905 │
│ .github/workflows/linux.yml               │        1013 │
│ .github/PULL_REQUEST_TEMPLATE.md          │        1093 │
│ dist/build/deploy.sh                      │        1291 │
│ docs/drawio/UI.drawio                     │        1993 │
│ CODE_OF_CONDUCT.md                        │        3368 │
│ CONTRIBUTING.md                           │       10756 │
│ assets/images/bookmarks.gif               │      298453 │
│ assets/images/auth.gif                    │      321769 │
│ assets/images/explorer.gif                │      650583 │
│ assets/images/config.gif                  │      705780 │
│ assets/images/text-editor.gif             │     1898000 │
└───────────────────────────────────────────┴─────────────┘
Saved 85% or 3.9 MB in 25 files (of 4.6 MB and 75 files in entire crate)
```

Please let me know if you would like some other files to be included
or whatever else is needed to make this PR mergeable.

Thanks :).
2021-01-11 09:09:50 +08:00
56 changed files with 934 additions and 293 deletions

3
.gitignore vendored
View File

@@ -18,3 +18,6 @@
*.rpm
*.deb
dist/pkgs/arch/*.tar.gz
# Macos
.DS_Store

View File

@@ -1,6 +1,7 @@
# Changelog
- [Changelog](#changelog)
- [0.3.1](#031)
- [0.3.0](#030)
- [0.2.0](#020)
- [0.1.3](#013)
@@ -10,6 +11,19 @@
---
## 0.3.1
Released on 18/01/2021
- **Keyring to store secrets**
- On both MacOS and Windows, the secret used to encrypt passwords in bookmarks it is now store in the OS secret vault. This provides much more security to store the password
- Enhancements:
- Added connection timeout to 30 seconds to SFTP/SCP clients and improved name lookup system.
- Bugfix:
- Solved index in explorer files list which was no more kept after 0.3.0
- SCP file transfer: fixed possible wrong file size when sending file, due to a possible incoherent size between the file explorer and the actual file size.
- Breaking changes: on **MacOS / Windows systems only**, the password you saved for bookmarks won't be working anymore if you have support for the keyring crate. Because of the migration to keyring, the previously used secret hasn't been migrated to the storage, instead a new secret will be used. To solve this, just save the bookmark again with the password.
## 0.3.0
Released on 10/01/2021

207
Cargo.lock generated
View File

@@ -1,5 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "aes"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561"
dependencies = [
"aes-soft",
"aesni",
"cipher",
]
[[package]]
name = "aes-soft"
version = "0.6.4"
@@ -10,6 +21,16 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "aesni"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce"
dependencies = [
"cipher",
"opaque-debug",
]
[[package]]
name = "aho-corasick"
version = "0.7.15"
@@ -205,16 +226,32 @@ dependencies = [
"memchr",
]
[[package]]
name = "core-foundation"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
dependencies = [
"core-foundation-sys 0.7.0",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
dependencies = [
"core-foundation-sys",
"core-foundation-sys 0.8.2",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]]
name = "core-foundation-sys"
version = "0.8.2"
@@ -272,6 +309,25 @@ dependencies = [
"winapi",
]
[[package]]
name = "crypto-mac"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6"
dependencies = [
"generic-array 0.14.4",
"subtle",
]
[[package]]
name = "dbus"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4a0c10ea61042b7555729ab0608727bbbb06ce709c11e6047cfa4e10f6d052d"
dependencies = [
"libc",
]
[[package]]
name = "debug-helper"
version = "0.3.10"
@@ -423,6 +479,26 @@ dependencies = [
"wasi 0.10.0+wasi-snapshot-preview1",
]
[[package]]
name = "hkdf"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
dependencies = [
"digest 0.9.0",
"hmac",
]
[[package]]
name = "hmac"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [
"crypto-mac",
"digest 0.9.0",
]
[[package]]
name = "hostname"
version = "0.3.1"
@@ -452,6 +528,18 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keyring"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bcd64f48199f69993c705fd2f76882e53969db93bc6345021bc8bb6462a9ffa"
dependencies = [
"byteorder",
"secret-service",
"security-framework 0.4.4",
"winapi",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -594,8 +682,8 @@ dependencies = [
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"security-framework 2.0.0",
"security-framework-sys 2.0.0",
"tempfile",
]
@@ -608,6 +696,40 @@ dependencies = [
"winapi",
]
[[package]]
name = "num"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.44"
@@ -618,6 +740,29 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
@@ -908,6 +1053,35 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "secret-service"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d752040301c251d653aa740dec847e95767ce312cfc469bee85eb13cbf81d8a"
dependencies = [
"aes",
"block-modes",
"dbus",
"hkdf",
"lazy_static",
"num",
"rand 0.7.3",
"sha2",
]
[[package]]
name = "security-framework"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
dependencies = [
"bitflags",
"core-foundation 0.7.0",
"core-foundation-sys 0.7.0",
"libc",
"security-framework-sys 0.4.3",
]
[[package]]
name = "security-framework"
version = "2.0.0"
@@ -915,10 +1089,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"core-foundation 0.9.1",
"core-foundation-sys 0.8.2",
"libc",
"security-framework-sys 2.0.0",
]
[[package]]
name = "security-framework-sys"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405"
dependencies = [
"core-foundation-sys 0.7.0",
"libc",
"security-framework-sys",
]
[[package]]
@@ -927,7 +1111,7 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b"
dependencies = [
"core-foundation-sys",
"core-foundation-sys 0.8.2",
"libc",
]
@@ -1019,6 +1203,12 @@ dependencies = [
"parking_lot 0.10.2",
]
[[package]]
name = "subtle"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
[[package]]
name = "syn"
version = "1.0.58"
@@ -1046,7 +1236,7 @@ dependencies = [
[[package]]
name = "termscp"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"bitflags",
"bytesize",
@@ -1058,6 +1248,7 @@ dependencies = [
"ftp4",
"getopts",
"hostname",
"keyring",
"lazy_static",
"magic-crypt",
"rand 0.8.1",

View File

@@ -1,6 +1,6 @@
[package]
name = "termscp"
version = "0.3.0"
version = "0.3.1"
authors = ["Christian Visintin"]
edition = "2018"
license = "GPL-3.0"
@@ -11,6 +11,7 @@ homepage = "https://github.com/veeso/termscp"
repository = "https://github.com/veeso/termscp"
documentation = "https://docs.rs/termscp"
readme = "README.md"
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -42,6 +43,9 @@ whoami = "1.0.1"
[target.'cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))'.dependencies]
users = "0.11.0"
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
keyring = "0.10.1"
[[bin]]
name = "termscp"
path = "src/main.rs"

View File

@@ -1,12 +1,12 @@
# TermSCP
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.3.0-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.3.1-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![Build](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![codecov](https://codecov.io/gh/veeso/termscp/branch/main/graph/badge.svg?token=au67l7nQah)](https://codecov.io/gh/veeso/termscp)
~ Basically, WinSCP on a terminal ~
Developed by Christian Visintin
Current version: 0.3.0 (10/01/2021)
Current version: 0.3.1 (18/01/2021)
---
@@ -84,10 +84,17 @@ If you want to contribute to this project, don't forget to check out our contrib
cargo install termscp
```
Requirements:
- Linux
- pkg-config
- libssh2
- openssl
### Deb package 📦
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.0_amd64.deb)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.0_amd64.deb`
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.1_amd64.deb)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.1_amd64.deb`
then install through dpkg:
@@ -99,8 +106,8 @@ gdebi termscp_*.deb
### RPM package 📦
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.0-1.x86_64.rpm)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.0-1.x86_64.rpm`
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.1-1.x86_64.rpm)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.1-1.x86_64.rpm`
then install through rpm:
@@ -126,7 +133,7 @@ Start PowerShell as administrator and run
choco install termscp
```
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.0.nupkg)
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.1.nupkg)
and then with PowerShell started with administrator previleges, run:
@@ -226,7 +233,12 @@ If you go to [gallery](#gallery-), there is a GIF showing how bookmarks work
### Are my passwords Safe 😈
Well, kinda.
As said before, bookmarks are saved in your configuration directory along with passwords. Passwords are obviously not plain text, they are encrypted with **AES-128**. Does this make them safe? Well, no, the key used to encrypt your passwords is generated at the first launch of termscp and stored on your drive. So it's still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉.
As said before, bookmarks are saved in your configuration directory along with passwords. Passwords are obviously not plain text, they are encrypted with **AES-128**. Does this make them safe? Well, depends on your operating system:
On Windows and MacOS the passwords are (if possible, but should be) in respectively the Windows Vault and the Keychain. This is actually super-safe and is directly managed by your operating system.
On Linux and BSD, on the other hand the key used to encrypt your passwords is stored on your drive. So it's still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉.
Actually [keyring-rs](https://github.com/hwchen/keyring-rs), supports Linux, but for different reasons I preferred not to make it available for this configuration. If you want to read more about my decision read [this issue](https://github.com/veeso/termscp/issues/2), while if you think this might have been implemented differently feel free to open an issue with your proposal.
---
@@ -350,6 +362,7 @@ TermSCP is powered by these aweseome projects:
- [bytesize](https://github.com/hyunsik/bytesize)
- [crossterm](https://github.com/crossterm-rs/crossterm)
- [edit](https://github.com/milkey-mouse/edit)
- [keyring-rs](https://github.com/hwchen/keyring-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-ftp](https://github.com/mattnenterprise/rust-ftp)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)

View File

@@ -1,14 +1,14 @@
pkgbase = termscp-bin
pkgdesc = TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal.
pkgver = 0.3.0
pkgver = 0.3.1
pkgrel = 1
url = https://github.com/veeso/termscp
arch = x86_64
license = GPL-3.0
provides = termscp
options = strip
source = https://github.com/veeso/termscp/releases/download/v0.3.0/termscp-0.3.0-x86_64.tar.gz
sha256sums = c9e777c48e30ff1ebf84dbe10f5471b2da753e324753bda2cb08109beab0637d
source = https://github.com/veeso/termscp/releases/download/v0.3.1/termscp-0.3.1-x86_64.tar.gz
sha256sums = dd056531554737595cbe5ac9ff741fdabf5e386299bbc0c81ea9e0f00fbbe2d0
pkgname = termscp-bin

View File

@@ -1,6 +1,6 @@
# Maintainer: Christian Visintin
pkgname=termscp
pkgver=0.3.0
pkgver=0.3.1
pkgrel=1
pkgdesc="TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal."
url="https://github.com/veeso/termscp"
@@ -9,7 +9,7 @@ arch=("x86_64")
provides=("termscp")
options=("strip")
source=("https://github.com/veeso/termscp/releases/download/v$pkgver/termscp-$pkgver-x86_64.tar.gz")
sha256sums=("c9e777c48e30ff1ebf84dbe10f5471b2da753e324753bda2cb08109beab0637d")
sha256sums=("dd056531554737595cbe5ac9ff741fdabf5e386299bbc0c81ea9e0f00fbbe2d0")
package() {
install -Dm755 termscp -t "$pkgdir/usr/bin/"

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -27,7 +27,7 @@ use std::path::PathBuf;
// Deps
use crate::filetransfer::FileTransferProtocol;
use crate::host::Localhost;
use crate::host::{HostError, Localhost};
use crate::ui::activities::{
auth_activity::AuthActivity, filetransfer_activity::FileTransferActivity,
filetransfer_activity::FileTransferParams, setup_activity::SetupActivity, Activity,
@@ -60,11 +60,11 @@ impl ActivityManager {
/// ### new
///
/// Initializes a new Activity Manager
pub fn new(local_dir: &PathBuf, interval: Duration) -> Result<ActivityManager, ()> {
pub fn new(local_dir: &PathBuf, interval: Duration) -> Result<ActivityManager, HostError> {
// Prepare Context
let host: Localhost = match Localhost::new(local_dir.clone()) {
Ok(h) => h,
Err(_) => return Err(()),
Err(e) => return Err(e),
};
let ctx: Context = Context::new(host);
Ok(ActivityManager {

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -36,9 +36,12 @@ use crate::utils::parser::{parse_datetime, parse_lstime};
use ftp4::native_tls::TlsConnector;
use ftp4::FtpStream;
use regex::Regex;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use std::{
io::{Read, Write},
ops::Range,
};
/// ## FtpFileTransfer
///
@@ -105,61 +108,28 @@ impl FtpFileTransfer {
if metadata.get(2).unwrap().as_str().len() < 9 {
return Err(());
}
// Get unix pex
let unix_pex: (u8, u8, u8) = {
let owner_pex: u8 = {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[0..3].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
let pex = |range: Range<usize>| {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[range].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
count
};
let group_pex: u8 = {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[3..6].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
}
count
};
let others_pex: u8 = {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[6..9].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
}
count
};
(owner_pex, group_pex, others_pex)
}
count
};
// Get unix pex
let unix_pex = (pex(0..3), pex(3..6), pex(6..9));
// Parse mtime and convert to SystemTime
let mtime: SystemTime = match parse_lstime(
metadata.get(7).unwrap().as_str(),
@@ -180,10 +150,12 @@ impl FtpFileTransfer {
Err(_) => None,
};
// Get filesize
let filesize: usize = match metadata.get(6).unwrap().as_str().parse::<usize>() {
Ok(sz) => sz,
Err(_) => 0,
};
let filesize: usize = metadata
.get(6)
.unwrap()
.as_str()
.parse::<usize>()
.unwrap_or(0);
let file_name: String = String::from(metadata.get(8).unwrap().as_str());
// Check if file_name is '.' or '..'
if file_name.as_str() == "." || file_name.as_str() == ".." {
@@ -270,10 +242,7 @@ impl FtpFileTransfer {
true => 0, // If is directory, filesize is 0
false => match metadata.get(3) {
// If is file, parse arg 3
Some(val) => match val.as_str().parse::<usize>() {
Ok(sz) => sz,
Err(_) => 0,
},
Some(val) => val.as_str().parse::<usize>().unwrap_or(0),
None => 0, // Should not happen
},
};

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -37,9 +37,10 @@ use crate::utils::parser::parse_lstime;
use regex::Regex;
use ssh2::{Channel, Session};
use std::io::{BufReader, BufWriter, Read, Write};
use std::net::TcpStream;
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use std::time::{Duration, SystemTime};
/// ## ScpFileTransfer
///
@@ -92,61 +93,28 @@ impl ScpFileTransfer {
if metadata.get(2).unwrap().as_str().len() < 9 {
return Err(());
}
// Get unix pex
let unix_pex: (u8, u8, u8) = {
let owner_pex: u8 = {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[0..3].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
let pex = |range: Range<usize>| {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[range].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
count
};
let group_pex: u8 = {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[3..6].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
}
count
};
let others_pex: u8 = {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[6..9].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
}
count
};
(owner_pex, group_pex, others_pex)
}
count
};
// Get unix pex
let unix_pex = (pex(0..3), pex(3..6), pex(6..9));
// Parse mtime and convert to SystemTime
let mtime: SystemTime = match parse_lstime(
metadata.get(7).unwrap().as_str(),
@@ -167,10 +135,7 @@ impl ScpFileTransfer {
Err(_) => None,
};
// Get filesize
let filesize: usize = match metadata.get(6).unwrap().as_str().parse::<usize>() {
Ok(sz) => sz,
Err(_) => 0,
};
let filesize: usize = metadata.get(6).unwrap().as_str().parse::<usize>().unwrap_or(0);
// Get link and name
let (file_name, symlink_path): (String, Option<PathBuf>) = match is_symlink {
true => self.get_name_and_link(metadata.get(8).unwrap().as_str()),
@@ -311,12 +276,34 @@ impl FileTransfer for ScpFileTransfer {
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
// Setup tcp stream
let tcp: TcpStream = match TcpStream::connect(format!("{}:{}", address, port)) {
Ok(stream) => stream,
Err(err) => {
let socket_addresses: Vec<SocketAddr> =
match format!("{}:{}", address, port).to_socket_addrs() {
Ok(s) => s.collect(),
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::BadAddress,
format!("{}", err),
))
}
};
let mut tcp: Option<TcpStream> = None;
// Try addresses
for socket_addr in socket_addresses.iter() {
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
Ok(stream) => {
tcp = Some(stream);
break;
}
Err(_) => continue,
}
}
// If stream is None, return connection timeout
let tcp: TcpStream = match tcp {
Some(t) => t,
None => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::BadAddress,
format!("{}", err),
FileTransferErrorType::ConnectionError,
String::from("Connection timeout"),
))
}
};
@@ -771,7 +758,13 @@ impl FileTransfer for ScpFileTransfer {
};
(mtime, atime)
};
match session.scp_send(file_name, mode, local.size as u64, Some(times)) {
// We need to get the size of local; NOTE: don't use the `size` attribute, since might be out of sync
let file_size: u64 = match std::fs::metadata(local.abs_path.as_path()) {
Ok(metadata) => metadata.len(),
Err(_) => local.size as u64, // NOTE: fallback to fsentry size
};
// Send file
match session.scp_send(file_name, mode, file_size, Some(times)) {
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -34,7 +34,7 @@ use crate::system::sshkey_storage::SshKeyStorage;
// Includes
use ssh2::{FileStat, OpenFlags, OpenType, Session, Sftp};
use std::io::{BufReader, BufWriter, Read, Write};
use std::net::TcpStream;
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
@@ -203,12 +203,34 @@ impl FileTransfer for SftpFileTransfer {
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
// Setup tcp stream
let tcp: TcpStream = match TcpStream::connect(format!("{}:{}", address, port)) {
Ok(stream) => stream,
Err(err) => {
let socket_addresses: Vec<SocketAddr> =
match format!("{}:{}", address, port).to_socket_addrs() {
Ok(s) => s.collect(),
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::BadAddress,
format!("{}", err),
))
}
};
let mut tcp: Option<TcpStream> = None;
// Try addresses
for socket_addr in socket_addresses.iter() {
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
Ok(stream) => {
tcp = Some(stream);
break;
}
Err(_) => continue,
}
}
// If stream is None, return connection timeout
let tcp: TcpStream = match tcp {
Some(t) => t,
None => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::BadAddress,
format!("{}", err),
FileTransferErrorType::ConnectionError,
String::from("Connection timeout"),
))
}
};

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -30,6 +30,7 @@ extern crate bitflags;
// Locals
use super::FsEntry;
// Ext
use std::cmp::Reverse;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@@ -137,7 +138,7 @@ impl FileExplorer {
///
/// Iterate over files
/// Filters are applied based on current options (e.g. hidden files not returned)
pub fn iter_files(&self) -> Box<dyn Iterator<Item = &FsEntry> + '_> {
pub fn iter_files(&self) -> impl Iterator<Item = &FsEntry> + '_ {
// Filter
let opts: ExplorerOpts = self.opts;
Box::new(self.files.iter().filter(move |x| {
@@ -154,7 +155,7 @@ impl FileExplorer {
/// ### iter_files_all
///
/// Iterate all files; doesn't care about options
pub fn iter_files_all(&self) -> Box<dyn Iterator<Item = &FsEntry> + '_> {
pub fn iter_files_all(&self) -> impl Iterator<Item = &FsEntry> + '_ {
Box::new(self.files.iter())
}
@@ -238,16 +239,14 @@ impl FileExplorer {
///
/// Sort files by creation time; the newest comes first
fn sort_files_by_creation_time(&mut self) {
self.files
.sort_by(|a: &FsEntry, b: &FsEntry| b.get_creation_time().cmp(&a.get_creation_time()));
self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time()));
}
/// ### sort_files_by_size
///
/// Sort files by size
fn sort_files_by_size(&mut self) {
self.files
.sort_by(|a: &FsEntry, b: &FsEntry| b.get_size().cmp(&a.get_size()));
self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_size()));
}
/// ### sort_files_directories_first
@@ -432,6 +431,19 @@ impl FileExplorer {
}
}
/// ### set_abs_index
///
/// Set absolute index
pub fn set_abs_index(&mut self, idx: usize) {
self.index = match idx >= self.files.len() {
true => match self.files.len() {
0 => 0,
_ => self.files.len() - 1,
},
false => idx,
};
}
/// ### toggle_hidden_files
///
/// Enable/disable hidden files
@@ -726,6 +738,33 @@ mod tests {
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
}
#[test]
fn test_fs_explorer_set_abs_index() {
let mut explorer: FileExplorer = FileExplorer::default();
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
// Create files (files are then sorted by name DEFAULT)
explorer.set_files(vec![
make_fs_entry("README.md", false),
make_fs_entry("src/", true),
make_fs_entry(".git/", true),
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("CODE_OF_CONDUCT.md", false),
make_fs_entry("CHANGELOG.md", false),
make_fs_entry("LICENSE", false),
make_fs_entry("Cargo.toml", false),
make_fs_entry("Cargo.lock", false),
make_fs_entry("codecov.yml", false),
make_fs_entry(".gitignore", false),
]);
explorer.set_abs_index(3);
assert_eq!(explorer.get_index(), 3);
explorer.set_abs_index(12);
assert_eq!(explorer.get_index(), 10);
explorer.set_files(vec![]);
explorer.set_abs_index(12);
assert_eq!(explorer.get_index(), 0);
}
#[test]
fn test_fs_explorer_sort_by_creation_time() {
let mut explorer: FileExplorer = FileExplorer::default();

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -1,6 +1,6 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -1,6 +1,6 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -167,8 +167,8 @@ fn main() {
// Create activity manager (and context too)
let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) {
Ok(m) => m,
Err(_) => {
eprintln!("Invalid directory '{}'", wrkdir.display());
Err(err) => {
eprintln!("Could not start activity manager: {}", err);
std::process::exit(255);
}
};

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -23,6 +23,12 @@
*
*/
// Deps
extern crate whoami;
// Crate
#[cfg(any(target_os = "windows", target_os = "macos"))]
use super::keys::keyringstorage::KeyringStorage;
use super::keys::{filestorage::FileStorage, KeyStorage, KeyStorageError};
// Local
use crate::bookmarks::serializer::BookmarkSerializer;
use crate::bookmarks::{Bookmark, SerializerError, SerializerErrorKind, UserHosts};
@@ -31,8 +37,7 @@ use crate::utils::crypto;
use crate::utils::fmt::fmt_time;
use crate::utils::random::random_alphanumeric_with_len;
// Ext
use std::fs::{OpenOptions, Permissions};
use std::io::{Read, Write};
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
@@ -53,23 +58,60 @@ impl BookmarksClient {
///
/// Instantiates a new BookmarksClient
/// Bookmarks file path must be provided
/// Key file must be provided
/// Storage path for file provider must be provided
pub fn new(
bookmarks_file: &Path,
key_file: &Path,
storage_path: &Path,
recents_size: usize,
) -> Result<BookmarksClient, SerializerError> {
// Create default hosts
let default_hosts: UserHosts = Default::default();
// If key file doesn't exist, create key, otherwise read it
let key: String = match key_file.exists() {
true => match BookmarksClient::load_key(key_file) {
Ok(key) => key,
Err(err) => return Err(err),
},
false => match BookmarksClient::generate_key(key_file) {
Ok(key) => key,
Err(err) => return Err(err),
// Make a key storage (windows / macos)
#[cfg(any(target_os = "windows", target_os = "macos"))]
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
let username: String = whoami::username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
// Check if keyring storage is supported
#[cfg(not(test))]
let app_name: &str = "termscp";
#[cfg(test)] // NOTE: when running test, add -test
let app_name: &str = "termscp-test";
match storage.is_supported() {
true => (Box::new(storage), app_name),
false => (Box::new(FileStorage::new(storage_path)), "bookmarks"),
}
};
// Make a key storage (linux / unix)
#[cfg(any(target_os = "linux", target_os = "unix"))]
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
#[cfg(not(test))]
let app_name: &str = "bookmarks";
#[cfg(test)] // NOTE: when running test, add -test
let app_name: &str = "bookmarks-test";
(Box::new(FileStorage::new(storage_path)), app_name)
};
// Load key
let key: String = match key_storage.get_key(service_id) {
Ok(k) => k,
Err(e) => match e {
KeyStorageError::NoSuchKey => {
// If no such key, generate key and set it into the storage
let key: String = Self::generate_key();
if let Err(e) = key_storage.set_key(service_id, key.as_str()) {
return Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
format!("Could not write key to storage: {}", e),
));
}
// Return key
key
}
_ => {
return Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
format!("Could not get key from storage: {}", e),
))
}
},
};
let mut client: BookmarksClient = BookmarksClient {
@@ -96,7 +138,7 @@ impl BookmarksClient {
/// ### iter_bookmarks
///
/// Iterate over bookmarks keys
pub fn iter_bookmarks(&self) -> Box<dyn Iterator<Item = &String> + '_> {
pub fn iter_bookmarks(&self) -> impl Iterator<Item = &String> + '_ {
Box::new(self.hosts.bookmarks.keys())
}
@@ -156,7 +198,7 @@ impl BookmarksClient {
/// ### iter_recents
///
/// Iterate over recents keys
pub fn iter_recents(&self) -> Box<dyn Iterator<Item = &String> + '_> {
pub fn iter_recents(&self) -> impl Iterator<Item = &String> + '_ {
Box::new(self.hosts.recents.keys())
}
@@ -276,36 +318,10 @@ impl BookmarksClient {
/// ### generate_key
///
/// Generate a new AES key and write it to key file
fn generate_key(key_file: &Path) -> Result<String, SerializerError> {
/// Generate a new AES key
fn generate_key() -> String {
// Generate 256 bytes (2048 bits) key
let key: String = random_alphanumeric_with_len(256);
// Write file
match OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(key_file)
{
Ok(mut file) => {
// Write key to file
if let Err(err) = file.write_all(key.as_bytes()) {
return Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
));
}
// Set file to readonly
let mut permissions: Permissions = file.metadata().unwrap().permissions();
permissions.set_readonly(true);
let _ = file.set_permissions(permissions);
Ok(key)
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
}
random_alphanumeric_with_len(256)
}
/// ### make_bookmark
@@ -331,28 +347,6 @@ impl BookmarksClient {
}
}
/// ### load_key
///
/// Load key from key_file
fn load_key(key_file: &Path) -> Result<String, SerializerError> {
match OpenOptions::new().read(true).open(key_file) {
Ok(mut file) => {
let mut key: String = String::with_capacity(256);
match file.read_to_string(&mut key) {
Ok(_) => Ok(key),
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
}
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
}
}
/// ### encrypt_str
///
/// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64
@@ -375,6 +369,7 @@ impl BookmarksClient {
}
#[cfg(test)]
#[cfg(not(target_os = "macos"))] // CI/CD blocks
mod tests {
use super::*;
@@ -382,6 +377,7 @@ mod tests {
use std::time::Duration;
#[test]
fn test_system_bookmarks_new() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
@@ -397,6 +393,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "linux"))]
fn test_system_bookmarks_new_err() {
assert!(BookmarksClient::new(
Path::new("/tmp/oifoif/omar"),
@@ -413,6 +410,7 @@ mod tests {
}
#[test]
fn test_system_bookmarks_new_from_existing() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
@@ -458,6 +456,7 @@ mod tests {
}
#[test]
fn test_system_bookmarks_manipulate_bookmarks() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
@@ -503,6 +502,7 @@ mod tests {
#[test]
#[should_panic]
fn test_system_bookmarks_bad_bookmark_name() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
@@ -521,6 +521,7 @@ mod tests {
}
#[test]
fn test_system_bookmarks_manipulate_recents() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
@@ -555,6 +556,7 @@ mod tests {
}
#[test]
fn test_system_bookmarks_dup_recent() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
@@ -579,6 +581,7 @@ mod tests {
}
#[test]
fn test_system_bookmarks_recents_more_than_limit() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
@@ -626,6 +629,7 @@ mod tests {
#[test]
#[should_panic]
fn test_system_bookmarks_add_bookmark_empty() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
@@ -646,10 +650,10 @@ mod tests {
/// ### get_paths
///
/// Get paths for configuration and key for bookmarks
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
let mut k: PathBuf = PathBuf::from(dir);
let k: PathBuf = PathBuf::from(dir);
let mut c: PathBuf = k.clone();
k.push("bookmarks.key");
c.push("bookmarks.toml");
(c, k)
}
@@ -657,6 +661,7 @@ mod tests {
/// ### create_tmp_dir
///
/// Create temporary directory
fn create_tmp_dir() -> tempfile::TempDir {
tempfile::TempDir::new().ok().unwrap()
}

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -242,7 +242,7 @@ impl ConfigClient {
/// ### iter_ssh_keys
///
/// Get an iterator through hosts in the ssh key storage
pub fn iter_ssh_keys(&self) -> Box<dyn Iterator<Item = &String> + '_> {
pub fn iter_ssh_keys(&self) -> impl Iterator<Item = &String> + '_ {
Box::new(self.config.remote.ssh_keys.keys())
}

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -59,14 +59,12 @@ pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
/// ### get_bookmarks_paths
///
/// Get paths for bookmarks client
/// Returns: path of bookmarks.toml and path of key
pub fn get_bookmarks_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
/// Returns: path of bookmarks.toml
pub fn get_bookmarks_paths(config_dir: &Path) -> PathBuf {
// Prepare paths
let mut bookmarks_file: PathBuf = PathBuf::from(config_dir);
bookmarks_file.push("bookmarks.toml");
let mut key_file: PathBuf = PathBuf::from(config_dir);
key_file.push(".bookmarks.key"); // key file is hidden
(bookmarks_file, key_file)
bookmarks_file
}
/// ### get_config_paths
@@ -123,10 +121,7 @@ mod tests {
fn test_system_environment_get_bookmarks_paths() {
assert_eq!(
get_bookmarks_paths(&Path::new("/home/omar/.config/termscp/")),
(
PathBuf::from("/home/omar/.config/termscp/bookmarks.toml"),
PathBuf::from("/home/omar/.config/termscp/.bookmarks.key")
)
PathBuf::from("/home/omar/.config/termscp/bookmarks.toml"),
);
}

View File

@@ -0,0 +1,163 @@
//! ## FileStorage
//!
//! `filestorage` provides an implementation of the `KeyStorage` trait using a file
/*
*
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Local
use super::{KeyStorage, KeyStorageError};
// Ext
use std::fs::{OpenOptions, Permissions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
/// ## FileStorage
///
/// File storage is an implementation o the `KeyStorage` which uses a file to store the key
pub struct FileStorage {
dir_path: PathBuf,
}
impl FileStorage {
/// ### new
///
/// Instantiates a new `FileStorage`
pub fn new(dir_path: &Path) -> Self {
FileStorage {
dir_path: PathBuf::from(dir_path),
}
}
/// ### make_file_path
///
/// Make file path for key file from `dir_path` and the application id
fn make_file_path(&self, storage_id: &str) -> PathBuf {
let mut p: PathBuf = self.dir_path.clone();
let file_name = format!(".{}.key", storage_id);
p.push(file_name);
p
}
}
impl KeyStorage for FileStorage {
/// ### get_key
///
/// Retrieve key from the key storage.
/// The key might be acccess through an identifier, which identifies
/// the key in the storage
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError> {
let key_file: PathBuf = self.make_file_path(storage_id);
// Check if file exists
if !key_file.exists() {
return Err(KeyStorageError::NoSuchKey);
}
// Read key from file
match OpenOptions::new().read(true).open(key_file.as_path()) {
Ok(mut file) => {
let mut key: String = String::new();
match file.read_to_string(&mut key) {
Ok(_) => Ok(key),
Err(_) => Err(KeyStorageError::ProviderError),
}
}
Err(_) => Err(KeyStorageError::ProviderError),
}
}
/// ### set_key
///
/// Set the key into the key storage
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> {
let key_file: PathBuf = self.make_file_path(storage_id);
// Write key
match OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(key_file.as_path())
{
Ok(mut file) => {
// Write key to file
if file.write_all(key.as_bytes()).is_err() {
return Err(KeyStorageError::ProviderError);
}
// Set file to readonly
let mut permissions: Permissions = file.metadata().unwrap().permissions();
permissions.set_readonly(true);
let _ = file.set_permissions(permissions);
Ok(())
}
Err(_) => Err(KeyStorageError::ProviderError),
}
}
/// is_supported
///
/// Returns whether the key storage is supported on the host system
fn is_supported(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_system_keys_filestorage_make_dir() {
let storage: FileStorage = FileStorage::new(&Path::new("/tmp/"));
assert_eq!(
storage.make_file_path("bookmarks").as_path(),
Path::new("/tmp/.bookmarks.key")
);
}
#[test]
fn test_system_keys_filestorage_ok() {
let key_dir: tempfile::TempDir =
tempfile::TempDir::new().expect("Could not create tempdir");
let storage: FileStorage = FileStorage::new(key_dir.path());
// Supported
assert!(storage.is_supported());
let app_name: &str = "termscp";
let secret: &str = "Th15-15/My-Супер-Секрет";
// Secret should not exist
assert_eq!(
storage.get_key(app_name).err().unwrap(),
KeyStorageError::NoSuchKey
);
// Write secret
assert!(storage.set_key(app_name, secret).is_ok());
// Get secret
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
}
#[test]
fn test_system_keys_filestorage_err() {
let bad_dir: &Path = Path::new("/piro/poro/pero/");
let storage: FileStorage = FileStorage::new(bad_dir);
let app_name: &str = "termscp";
let secret: &str = "Th15-15/My-Супер-Секрет";
assert!(storage.set_key(app_name, secret).is_err());
}
}

View File

@@ -0,0 +1,129 @@
//! ## KeyringStorage
//!
//! `keyringstorage` provides an implementation of the `KeyStorage` trait using the OS keyring
/*
*
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Deps
extern crate keyring;
// Local
use super::{KeyStorage, KeyStorageError};
// Ext
use keyring::{Keyring, KeyringError};
/// ## KeyringStorage
///
/// provides a `KeyStorage` implementation using the keyring crate
pub struct KeyringStorage {
username: String,
}
impl KeyringStorage {
/// ### new
///
/// Instantiates a new KeyringStorage
pub fn new(username: &str) -> Self {
KeyringStorage {
username: username.to_string(),
}
}
}
impl KeyStorage for KeyringStorage {
/// ### get_key
///
/// Retrieve key from the key storage.
/// The key might be acccess through an identifier, which identifies
/// the key in the storage
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError> {
let storage: Keyring = Keyring::new(storage_id, self.username.as_str());
match storage.get_password() {
Ok(s) => Ok(s),
Err(e) => match e {
KeyringError::NoPasswordFound => Err(KeyStorageError::NoSuchKey),
#[cfg(target_os = "windows")]
KeyringError::WindowsVaultError => Err(KeyStorageError::NoSuchKey),
#[cfg(target_os = "macos")]
KeyringError::MacOsKeychainError(_) => Err(KeyStorageError::NoSuchKey),
_ => panic!("{}", e),
},
}
}
/// ### set_key
///
/// Set the key into the key storage
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> {
let storage: Keyring = Keyring::new(storage_id, self.username.as_str());
match storage.set_password(key) {
Ok(_) => Ok(()),
Err(_) => Err(KeyStorageError::ProviderError),
}
}
/// is_supported
///
/// Returns whether the key storage is supported on the host system
fn is_supported(&self) -> bool {
let dummy: String = String::from("dummy-service");
let storage: Keyring = Keyring::new(dummy.as_str(), self.username.as_str());
// Check what kind of error is returned
match storage.get_password() {
Ok(_) => true,
Err(err) => !matches!(err, KeyringError::NoBackendFound),
}
}
}
#[cfg(test)]
mod tests {
extern crate whoami;
use super::*;
use whoami::username;
#[test]
fn test_system_keys_keyringstorage() {
let username: String = username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
assert!(storage.is_supported());
let app_name: &str = "termscp-test2";
let secret: &str = "Th15-15/My-Супер-Секрет";
let kring: Keyring = Keyring::new(app_name, username.as_str());
let _ = kring.delete_password();
drop(kring);
// Secret should not exist
assert_eq!(
storage.get_key(app_name).err().unwrap(),
KeyStorageError::NoSuchKey
);
// Write secret
assert!(storage.set_key(app_name, secret).is_ok());
// Get secret
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
// Delete the key manually...
let kring: Keyring = Keyring::new(app_name, username.as_str());
assert!(kring.delete_password().is_ok());
}
}

90
src/system/keys/mod.rs Normal file
View File

@@ -0,0 +1,90 @@
//! ## KeyStorage
//!
//! `keystorage` provides the trait to manipulate to a KeyStorage
/*
*
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Storages
pub mod filestorage;
#[cfg(any(target_os = "windows", target_os = "macos"))]
pub mod keyringstorage;
/// ## KeyStorageError
///
/// defines the error type for the `KeyStorage`
#[derive(PartialEq, std::fmt::Debug)]
pub enum KeyStorageError {
//BadKey,
ProviderError,
NoSuchKey,
}
impl std::fmt::Display for KeyStorageError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let err: String = String::from(match &self {
//KeyStorageError::BadKey => "Bad key syntax",
KeyStorageError::ProviderError => "Provider service error",
KeyStorageError::NoSuchKey => "No such key",
});
write!(f, "{}", err)
}
}
/// ## KeyStorage
///
/// this traits provides the methods to communicate and interact with the key storage.
pub trait KeyStorage {
/// ### get_key
///
/// Retrieve key from the key storage.
/// The key might be acccess through an identifier, which identifies
/// the key in the storage
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError>;
/// ### set_key
///
/// Set the key into the key storage
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError>;
/// is_supported
///
/// Returns whether the key storage is supported on the host system
fn is_supported(&self) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_system_keys_mod_errors() {
assert_eq!(
format!("{}", KeyStorageError::ProviderError),
String::from("Provider service error")
);
assert_eq!(
format!("{}", KeyStorageError::NoSuchKey),
String::from("No such key")
);
}
}

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -27,4 +27,5 @@
pub mod bookmarks_client;
pub mod config_client;
pub mod environment;
pub(crate) mod keys;
pub mod sshkey_storage;

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -224,11 +224,15 @@ impl AuthActivity {
match environment::init_config_dir() {
Ok(path) => {
// If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system.
if let Some(path) = path {
let (bookmarks_file, key_file): (PathBuf, PathBuf) =
environment::get_bookmarks_paths(path.as_path());
if let Some(config_dir_path) = path {
let bookmarks_file: PathBuf =
environment::get_bookmarks_paths(config_dir_path.as_path());
// Initialize client
match BookmarksClient::new(bookmarks_file.as_path(), key_file.as_path(), 16) {
match BookmarksClient::new(
bookmarks_file.as_path(),
config_dir_path.as_path(),
16,
) {
Ok(cli) => self.bookmarks_client = Some(cli),
Err(err) => {
self.popup = Some(Popup::Alert(
@@ -236,7 +240,7 @@ impl AuthActivity {
format!(
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
bookmarks_file.display(),
key_file.display(),
config_dir_path.display(),
err
),
))

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -1,6 +1,6 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
@@ -600,9 +600,12 @@ impl FileTransferActivity {
match self.context.as_ref().unwrap().local.scan_dir(path) {
Ok(files) => {
// Set files and sort (sorting is implicit)
let prev_index: usize = self.local.get_index();
self.local.set_files(files);
// Restore index
self.local.set_abs_index(prev_index);
// Set index; keep if possible, otherwise set to last item
self.local.set_index(match self.local.get_current_file() {
self.local.set_abs_index(match self.local.get_current_file() {
Some(_) => self.local.get_index(),
None => match self.local.count() {
0 => 0,
@@ -626,9 +629,12 @@ impl FileTransferActivity {
match self.client.list_dir(path) {
Ok(files) => {
// Set files and sort (sorting is implicit)
let prev_index: usize = self.remote.get_index();
self.remote.set_files(files);
// Restore index
self.remote.set_abs_index(prev_index);
// Set index; keep if possible, otherwise set to last item
self.remote.set_index(match self.remote.get_current_file() {
self.remote.set_abs_index(match self.remote.get_current_file() {
Some(_) => self.remote.get_index(),
None => match self.remote.count() {
0 => 0,

View File

@@ -5,7 +5,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -5,7 +5,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -5,7 +5,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -5,7 +5,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -5,7 +5,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -5,7 +5,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -5,7 +5,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*

View File

@@ -4,7 +4,7 @@
/*
*
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*