diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2eacfee..7f4363f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,8 +83,28 @@ jobs: run: apk add --no-cache --update sudo openssh bash openssh-keygen gcc musl-dev rust cargo - name: add user run: addgroup ubuntu && adduser --shell /bin/ash --disabled-password --home /home/ubuntu --ingroup ubuntu ubuntu && echo "ubuntu:password" | chpasswd - - name: config ssh - run: ssh-keygen -A && sed -i -E "s|(AuthorizedKeysFile).*|\1 %h/.ssh/authorized_keys|g" /etc/ssh/sshd_config && echo "HostKeyAlgorithms=+ssh-rsa" >> /etc/ssh/sshd_config && echo "PubkeyAcceptedAlgorithms=+ssh-rsa" >> /etc/ssh/sshd_config && echo "KexAlgorithms=+diffie-hellman-group14-sha1,diffie-hellman-group1-sha1" >> /etc/ssh/sshd_config && sed -i -E "s/#?(ChallengeResponseAuthentication|PasswordAuthentication).*/\1 yes/g" /etc/ssh/sshd_config + - name: config ssh keys + run: ssh-keygen -A + - name: generate dsa keys + run: ssh-keygen -t dsa -b 1024 -N '' -f /etc/ssh/ssh_host_dsa_key + - name: add pubkey authentication + run: sed -i -E "s|(AuthorizedKeysFile).*|\1 %h/.ssh/authorized_keys|g" /etc/ssh/sshd_config + - name: enable password authentication + run: sed -i -E "s/#?(ChallengeResponseAuthentication|PasswordAuthentication).*/\1 yes/g" /etc/ssh/sshd_config + - name: add deprecated pubkeys + run: echo "HostKeyAlgorithms=+ssh-rsa,ssh-dss" >> /etc/ssh/sshd_config && echo "PubkeyAcceptedAlgorithms=+ssh-rsa,ssh-dss" >> /etc/ssh/sshd_config + - name: add deprecated kexes + run: echo "KexAlgorithms=+diffie-hellman-group14-sha1,diffie-hellman-group1-sha1" >> /etc/ssh/sshd_config + - name: add deprecated ciphers + run: echo "Ciphers=+aes128-cbc,3des-cbc,aes192-cbc,aes256-cbc" >> /etc/ssh/sshd_config + - name: add deprecated dsa keys + run: echo "HostKey /etc/ssh/ssh_host_dsa_key" >> /etc/ssh/sshd_config + - name: add rsa keys + run: echo "HostKey /etc/ssh/ssh_host_rsa_key" >> /etc/ssh/sshd_config + - name: add ed25519 keys + run: echo "HostKey /etc/ssh/ssh_host_ed25519_key" >> /etc/ssh/sshd_config + - name: add ecdsa keys + run: echo "HostKey /etc/ssh/ssh_host_ecdsa_key" >> /etc/ssh/sshd_config - name: create .ssh run: mkdir -p /home/ubuntu/.ssh && umask 066; touch /home/ubuntu/.ssh/authorized_keys - name: generate rsa files @@ -98,6 +118,6 @@ jobs: - name: run ssh run: mkdir /run/sshd && /usr/sbin/sshd -T &&/usr/sbin/sshd -D -p 8888 & - name: Test - run: cargo test --all-features + run: cargo test --all-features -- --test-threads 1 - name: Doc test run: cargo test --doc --all-features \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6e818ee..5d32368 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .idea target .gitignore +.vscode # Generated by Cargo # will have compiled files and executables /target/ diff --git a/Cargo.toml b/Cargo.toml index 0697c65..7f26a64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ssh-rs" -version = "0.3.2" +version = "0.4.5" edition = "2021" authors = [ "Gao Xiang Kang <1148118271@qq.com>", @@ -14,41 +14,65 @@ repository = "https://github.com/1148118271/ssh-rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -dangerous-algorithms = ["dangerous-rsa-sha1", "dangerous-dh-group1-sha1"] -dangerous-rsa-sha1 = ["sha1"] -dangerous-dh-group1-sha1 = [] +deprecated-algorithms = [ + "deprecated-rsa-sha1", + "deprecated-dh-group1-sha1", + "deprecated-aes-cbc", + "deprecated-des-cbc", + "deprecated-dss-sha1" + ] +deprecated-rsa-sha1 = ["dep:sha1"] +deprecated-dss-sha1 = ["dep:sha1", "dep:dsa"] +deprecated-dh-group1-sha1 = ["dep:sha1"] +deprecated-aes-cbc = ["dep:cbc", "dep:cipher"] +deprecated-des-cbc = ["dep:cbc", "dep:cipher", "dep:des"] +deprecated-zlib = [] +scp = ["dep:filetime"] + +[lib] +name = "ssh" +path = "src/lib.rs" [dependencies] -log = "0.4" +## error +thiserror = "^1.0" + +## log +tracing = { version = "0.1.36", features = ["log"] } + +## string enum +strum = "0.25" +strum_macros = "0.25" + +## algorithm rand = "0.8" num-bigint = { version = "0.4", features = ["rand"] } -strum = "0.24" -strum_macros = "0.24" # the crate rsa has removed the internal hash implement from 0.7.0 sha1 = { version = "0.10.5", default-features = false, features = ["oid"], optional = true } sha2 = { version = "0.10.6", default-features = false, features = ["oid"]} -rsa = "^0.7" -aes = { version = "0.7", features = ["ctr"] } -ssh-key = { version = "0.5.1", features = ["rsa", "ed25519"]} -signature = "1.6.4" -ring = "0.16.20" -filetime = "0.2" +dsa = { version = "0.6.1", optional = true } +rsa = "0.9" +aes = "0.8" +ctr = "0.9" +des = { version = "0.8", optional = true } +cbc = { version = "0.1", optional = true } +cipher = { version = "0.4", optional = true } +ssh-key = { version = "0.6", features = ["rsa", "ed25519", "alloc"]} +signature = "2.1" +ring = "0.17" + +## compression +flate2 = "^1.0" -# async -# [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -# tokio = { version = "^1", features = ["full"] } +## utils +filetime = { version = "0.2", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2", features = ["js"] } -# tokio = { version = "^1", features = [ -# "sync", -# "macros", -# "io-util", -# "rt", -# "time" -# ]} +ring = { version = "0.17", features = ["wasm32_unknown_unknown_js"] } + [dev-dependencies] +tracing-subscriber = { version = "^0.3" } paste = "1" diff --git a/README.md b/README.md index 50b88d8..12a8463 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ or [PR](https://github.com/1148118271/ssh-rs/pulls) . ### 1. Password: ```rust -use ssh_rs::ssh; +use ssh; + let mut session = ssh::create_session() .username("ubuntu") .password("password") @@ -67,7 +68,8 @@ let mut session = ssh::create_session() // and end with // -----END RSA PRIVATE KEY----- / -----END OPENSSH PRIVATE KEY----- // simply generated by `ssh-keygen -t rsa -m PEM -b 4096` -use ssh_rs::ssh; +use ssh; + let mut session = ssh::create_session() .username("ubuntu") .private_key_path("./id_rsa") @@ -82,7 +84,8 @@ let mut session = ssh::create_session() // -----BEGIN RSA PRIVATE KEY----- / -----BEGIN OPENSSH PRIVATE KEY----- // and end with // -----END RSA PRIVATE KEY----- / -----END OPENSSH PRIVATE KEY----- -use ssh_rs::ssh; +use ssh; + let mut session = ssh::create_session() .username("ubuntu") .private_key("rsa_string") @@ -95,7 +98,8 @@ let mut session = ssh::create_session() * According to the implementation of OpenSSH, it will try public key first and fallback to password. So both of them can be provided. ```Rust -use ssh_rs::ssh; +use ssh; + let mut session = ssh::create_session() .username("username") .password("password") @@ -106,16 +110,22 @@ let mut session = ssh::create_session() ## Enable global logging: -* There are two APIs to enable logs, basicly `enable_log()` will set the log level to `INFO`, and `debug()` will set it to `Debug` - -* But you can implement your own logger as well. +* This crate now uses the `log` compatible `tracing` for logging functionality ```rust -use ssh_rs::ssh; +use tracing::Level; +use tracing_subscriber::FmtSubscriber; + // this will generate some basic event logs -ssh::enable_log(); -// this will generate verbose logs -ssh::debug() +// a builder for `FmtSubscriber`. +let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + +tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); ``` ## Set timeout: @@ -123,16 +133,15 @@ ssh::debug() * Only global timeouts per r/w are currently supported. ```rust -use ssh_rs::ssh; +use ssh; -ssh::debug(); let _listener = TcpListener::bind("127.0.0.1:7777").unwrap(); match ssh::create_session() .username("ubuntu") .password("password") .private_key_path("./id_rsa") - .timeout(5 * 1000) + .timeout(Some(std::time::Duration::from_secs(5))) .connect("127.0.0.1:7777") { Err(e) => println!("Got error {}", e), @@ -159,44 +168,39 @@ match ssh::create_session() * `ecdh-sha2-nistp256` * `diffie-hellman-group14-sha256` * `diffie-hellman-group14-sha1` -* `diffie-hellman-group1-sha1` (behind feature "dangerous-dh-group1-sha1") +* `diffie-hellman-group1-sha1` (behind feature "deprecated-dh-group1-sha1") ### 2. Server host key algorithms * `ssh-ed25519` * `rsa-sha2-256` * `rsa-sha2-512` -* `rsa-sha` (behind feature "dangerous-rsa-sha1") - -### 3. Encryption algorithms (client to server) +* `rsa-sha` (behind feature "deprecated-rsa-sha1") +* `ssh-dss` (behind feature "deprecated-dss-sha1") -* `chacha20-poly1305@openssh.com` -* `aes128-ctr` -### 4. Encryption algorithms (server to client) +### 3. Encryption algorithms * `chacha20-poly1305@openssh.com` * `aes128-ctr` +* `aes192-ctr` +* `aes256-ctr` +* `aes128-cbc` (behind feature "deprecated-aes-cbc") +* `aes192-cbc` (behind feature "deprecated-aes-cbc") +* `aes256-cbc` (behind feature "deprecated-aes-cbc") +* `3des-cbc` (behind feature "deprecated-des-cbc") -### 5. Mac algorithms (client to server) +### 4. Mac algorithms * `hmac-sha2-256` * `hmac-sha2-512` * `hmac-sha1` -### 6. Mac algorithms (server to client) - -* `hmac-sha2-256` -* `hmac-sha2-512` -* `hmac-sha1` - -### 7. Compression algorithms (client to server) - -* `none` - -### 8. Compression algorithms (server to client) +### 5. Compression algorithms * `none` +* `zlib@openssh.com` +* `zlib` (behind feature "zlib") --- diff --git a/README_ZH.md b/README_ZH.md index 62d07f0..fdcd72c 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -49,25 +49,39 @@ fn main() { ``` ### 启用全局日志: - +本crate现在使用兼容`log`的`tracing` crate记录log +使用下面的代码片段启用log ```rust - ssh::debug(); +use tracing::Level; +use tracing_subscriber::FmtSubscriber; +// this will generate some basic event logs +// a builder for `FmtSubscriber`. +let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + +tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); ``` -### 设置超时时间: +### 设置全局超时时间: ```rust -ssh::create_session().timeout(50); +ssh::create_session().timeout(Some(std::time::Duration::from_secs(5))); ``` -### 目前只支持 exec shell scp 三种使用方式: -1. [exec示例](examples/exec/src/main.rs) -2. [shell示例](examples/shell/src/main.rs) -3. [scp示例](examples/scp/src/main.rs) +### 使用样例 +* 更多使用样例请参考[examples](examples)目录 +1. [执行单个命令](examples/exec/src/main.rs) +2. [通过scp传输文件](examples/scp/src/main.rs) +3. [启动一个pty](examples/shell/src/main.rs) +4. [运行一个交互式的shell](examples/shell_interactive/src/main.rs) +5. [使用非tcp连接](examples/bio/src/main.rs) +6. [自行配置密码组](examples/customized_algorithms/src/main.rs) -### 自定义连接方式: -[bio示例](examples/bio/src/main.rs) ### 算法支持: @@ -81,33 +95,30 @@ ssh::create_session().timeout(50); * `ssh-ed25519` * `rsa-sha2-512` * `rsa-sha2-256` -* `rsa-sha` (features = ["dangerous-rsa-sha1"]) - -#### 3. 加密算法(客户端到服务端) - -* `chacha20-poly1305@openssh.com` -* `aes128-ctr` +* `rsa-sha` (features = ["deprecated-rsa-sha1"]) +* `ssh-dss` (features = ["deprecated-dss-sha1"]) -#### 4. 加密算法(服务端到客户端) +#### 3. 加密算法 * `chacha20-poly1305@openssh.com` * `aes128-ctr` +* `aes192-ctr` +* `aes256-ctr` +* `aes128-cbc` (features = ["deprecated-aes-cbc"]) +* `aes192-cbc` (features = ["deprecated-aes-cbc"]) +* `aes256-cbc` (features = ["deprecated-aes-cbc"]) +* `3des-cbc` (features = ["deprecated-des-cbc"]) -#### 5. MAC算法(客户端到服务端) +#### 4. MAC算法 +* `hmac-sha2-256` +* `hmac-sha2-512` * `hmac-sha1` -#### 6. MAC算法(服务端到客户端) - -* `hmac-sha1` - -#### 7. 压缩算法(客户端到服务端) - -* `none` - -#### 8. 压缩算法(服务端到客户端) +#### 5. 压缩算法 * `none` +* `zlib` (behind feature "zlib") --- diff --git a/build.rs b/build.rs index d1910ef..8727018 100644 --- a/build.rs +++ b/build.rs @@ -13,38 +13,35 @@ fn main() { return; } - println!( - "cargo:warning= current version: {} last version: {}", - current_version, last_version - ); + println!("cargo:warning= current version: {current_version} last version: {last_version}"); - // 替换lib.rs + // lib.rs if replace_lib(¤t_version, &last_version).is_err() { return; } - // 替换constant.rs + // constant.rs if replace_constant(¤t_version, &last_version).is_err() { return; } - // 替换Cargo.toml + // Cargo.toml let _ = replace_cargo(¤t_version, &last_version); } fn replace_constant(current_version: &str, last_version: &str) -> Result<(), ()> { - let current_str = format!("SSH-2.0-SSH_RS-{}", current_version); - let last_str = format!("SSH-2.0-SSH_RS-{}", last_version); + let current_str = format!("SSH-2.0-SSH_RS-{current_version}"); + let last_str = format!("SSH-2.0-SSH_RS-{last_version}"); replace_file("src/constant.rs", current_str, last_str) } fn replace_lib(current_version: &str, last_version: &str) -> Result<(), ()> { - let current_str = format!("ssh-rs = \"{}\"", current_version); - let last_str = format!("ssh-rs = \"{}\"", last_version); + let current_str = format!("ssh-rs = \"{current_version}\""); + let last_str = format!("ssh-rs = \"{last_version}\""); replace_file("src/lib.rs", current_str, last_str) } fn replace_cargo(current_version: &str, last_version: &str) -> Result<(), ()> { - let current_str = format!("version = \"{}\"", current_version); - let last_str = format!("version = \"{}\"", last_version); + let current_str = format!("version = \"{current_version}\""); + let last_str = format!("version = \"{last_version}\""); replace_file("Cargo.toml", current_str, last_str) } diff --git a/build_check.sh b/build_check.sh index 30eb09a..906e845 100644 --- a/build_check.sh +++ b/build_check.sh @@ -5,6 +5,14 @@ cargo fmt --all -- --check > /dev/null echo done echo echo +echo clippy check +cargo clippy -- -D warnings > /dev/null +echo +echo +echo clippy all check +cargo clippy --all-features -- -D warnings > /dev/null +echo +echo echo linux build check cargo build --target x86_64-unknown-linux-gnu > /dev/null echo done @@ -21,5 +29,5 @@ echo done echo echo echo cargo test -cargo test > /dev/null +cargo test --all-features -- --test-threads 1 > /dev/null echo done \ No newline at end of file diff --git a/changelog b/changelog new file mode 100644 index 0000000..f2be1f1 --- /dev/null +++ b/changelog @@ -0,0 +1,86 @@ +v0.4.5 (2023-11-17) + 1. Fix the high cpu usage caused by non_block tcp + 2. Fix the failuer of version agreement if the server sends more than one lines + +v0.4.4 (2023-11-15) + 1. Remove some debug print + 2. Fix the panic when connect to non-ssh servers + 3. Start the ssh-version negotiations as soon as the connection established + +v0.4.3 (2023-10-18) + 1. Bump ring to 0.17 + 2. Add ssh-dss support (behind feature deprecated-dss-sha1) + +v0.4.2 (2023-10-13) + 1. Bump trace version, see #75 for more details + 2. Bugfix: Do not panic at non-ssh server connections, see #77 for more + details + +v0.4.1 (2023-09-20) + 1. Add zlib, zlib@openssh.com support + +v0.4.0 (2023-09-16) + 1. remove chinese comments + 2. add RFC links + 3. remove the self-implemented log, using tracing instead + 4. move scp related function behind feature `scp' + 5. re-implement the ssh-error to derive thiserror crate + 6. rename the dangerous-related features to deprecated-* + 7. add aes-128/192/256-cbc encryption modes (behind feature deprecated-aes-cbc) + 7. add 3des-cbc encryption modes (behind feature deprecated-des-cbc) + +v0.3.3 (2023-09-10) + 1. fix hang when tcp connects to a non-existent host + 2. refactor aes_ctr file + 3. translate the changelogs + 4. use std::time::Duration as timeout rather than u128 + 5. add the support for ssh message `SSH_MSG_CHANNEL_EXTENDED_DATA` + 6. bump dependencies + +v0.3.2 (2023-01-10) + 1. fix some error with hmac2 + 2. add aes-192-crt, aes-256-ctr + +v0.3.1 (2022-12-07) + fix some issues + +v0.3.0 (2022-11-18) + 1. code refactor + 2. disable ssh-rsa by default, move it behind feature "dangerous-algorithms" + +v0.2.2 (2022-11-05) + 1. add connect_bio API which allows connection over any read/write objects. + 2. implement key exchanges during a connected connection + +v0.2.1 (2022-09-26) + 1. fix sometimes unexpected timeout + +v0.2.0 (2022-08-29) + 1. add aes_ctr_128 + 2. add hmac_sha1 + 3. set tcp non-block by default + 4. add public_key auth + +v0.1.5 (2022-06-13) + 1. modify the accessibility of ChannelScp + +v0.1.4 (2022-05-31) + 1. remove all mutex + 2. fix issues with window size + 3. add scp upload & download + +v0.1.3 (2022-01-17): + 1. code refactor + 2. add log + 3. open channel directly from session + +v0.1.2 (2022-01-9): + 1. fix shell channel cannot be using among threads (Incompatible from ver 0.3) + 2. fix that one session cannot open multiple channels + 3. remove chrono + +v0.1.1 (2022-01-5): + 1. fix crashes + +v0.1.0 (2022-01-5): + 1. implement the basic ssh protocol diff --git a/examples/bio/Cargo.toml b/examples/bio/Cargo.toml index 8a4f135..16fe691 100644 --- a/examples/bio/Cargo.toml +++ b/examples/bio/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ssh-rs = { path = "../../" } \ No newline at end of file +ssh-rs = { path = "../../" } +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } diff --git a/examples/bio/src/main.rs b/examples/bio/src/main.rs index b61daf8..cfb280d 100644 --- a/examples/bio/src/main.rs +++ b/examples/bio/src/main.rs @@ -1,8 +1,18 @@ -use ssh_rs::ssh; + use std::net::{TcpStream, ToSocketAddrs}; +use tracing::Level; +use tracing_subscriber::FmtSubscriber; fn main() { - ssh::enable_log(); + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let bio = MyProxy::new("127.0.0.1:22"); diff --git a/examples/customized_algorithms/Cargo.toml b/examples/customized_algorithms/Cargo.toml index 76da4af..22b9c27 100644 --- a/examples/customized_algorithms/Cargo.toml +++ b/examples/customized_algorithms/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ssh-rs = { path="../../", features = ["dangerous-algorithms"]} \ No newline at end of file +ssh-rs = { path="../../", features = ["deprecated-algorithms"]} +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } diff --git a/examples/customized_algorithms/src/main.rs b/examples/customized_algorithms/src/main.rs index d5bb248..4f6d1d0 100644 --- a/examples/customized_algorithms/src/main.rs +++ b/examples/customized_algorithms/src/main.rs @@ -1,8 +1,17 @@ -use ssh_rs::algorithm; -use ssh_rs::ssh; +use ssh::algorithm; +use tracing::Level; +use tracing_subscriber::FmtSubscriber; fn main() { - ssh::enable_log(); + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let mut session = ssh::create_session_without_default() .username("ubuntu") diff --git a/examples/exec/Cargo.toml b/examples/exec/Cargo.toml index 9d5a8a0..a9bef66 100644 --- a/examples/exec/Cargo.toml +++ b/examples/exec/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ssh-rs = { path = "../../" } \ No newline at end of file +ssh-rs = { path = "../../" } +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } \ No newline at end of file diff --git a/examples/exec/src/main.rs b/examples/exec/src/main.rs index e6f97ba..b82b2d0 100644 --- a/examples/exec/src/main.rs +++ b/examples/exec/src/main.rs @@ -1,7 +1,17 @@ -use ssh_rs::ssh; + +use tracing::Level; +use tracing_subscriber::FmtSubscriber; fn main() { - ssh::enable_log(); + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let mut session = ssh::create_session() .username("ubuntu") @@ -14,5 +24,8 @@ fn main() { let vec: Vec = exec.send_command("ls -all").unwrap(); println!("{}", String::from_utf8(vec).unwrap()); // Close session. + let exec = session.open_exec().unwrap(); + let vec: Vec = exec.send_command("no_command").unwrap(); + println!("{}", String::from_utf8(vec).unwrap()); session.close(); } diff --git a/examples/exec_backend/Cargo.toml b/examples/exec_backend/Cargo.toml index 0bef84f..e6e66f4 100644 --- a/examples/exec_backend/Cargo.toml +++ b/examples/exec_backend/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ssh-rs = { path = "../../" } \ No newline at end of file +ssh-rs = { path = "../../" } +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } \ No newline at end of file diff --git a/examples/exec_backend/src/main.rs b/examples/exec_backend/src/main.rs index 5f9752d..9be9fc4 100644 --- a/examples/exec_backend/src/main.rs +++ b/examples/exec_backend/src/main.rs @@ -1,7 +1,17 @@ -use ssh_rs::ssh; + +use tracing::Level; +use tracing_subscriber::FmtSubscriber; fn main() { - ssh::enable_log(); + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let mut session = ssh::create_session() .username("ubuntu") diff --git a/examples/scp/Cargo.toml b/examples/scp/Cargo.toml index c2a244e..22f8daa 100644 --- a/examples/scp/Cargo.toml +++ b/examples/scp/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ssh-rs = { path = "../../" } +ssh-rs = { path = "../../", features = ["scp"] } +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } diff --git a/examples/scp/src/main.rs b/examples/scp/src/main.rs index b9c05b2..22265ef 100644 --- a/examples/scp/src/main.rs +++ b/examples/scp/src/main.rs @@ -1,6 +1,18 @@ -use ssh_rs::ssh; + +use tracing::Level; +use tracing_subscriber::FmtSubscriber; fn main() { + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + let mut session = ssh::create_session() .username("ubuntu") .password("password") diff --git a/examples/scp_backend/Cargo.toml b/examples/scp_backend/Cargo.toml index 6352a65..43bfd79 100644 --- a/examples/scp_backend/Cargo.toml +++ b/examples/scp_backend/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ssh-rs = { path = "../../" } +ssh-rs = { path = "../../", features = ["scp"] } +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } diff --git a/examples/scp_backend/src/main.rs b/examples/scp_backend/src/main.rs index 76b8879..5140a64 100644 --- a/examples/scp_backend/src/main.rs +++ b/examples/scp_backend/src/main.rs @@ -1,7 +1,17 @@ -use ssh_rs::ssh; + +use tracing::Level; +use tracing_subscriber::FmtSubscriber; fn main() { - ssh::enable_log(); + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let mut session = ssh::create_session() .username("ubuntu") .password("password") diff --git a/examples/shell/Cargo.toml b/examples/shell/Cargo.toml index 8e17841..7d47359 100644 --- a/examples/shell/Cargo.toml +++ b/examples/shell/Cargo.toml @@ -7,3 +7,5 @@ edition = "2021" [dependencies] ssh-rs = { path = "../../" } +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } diff --git a/examples/shell/src/main.rs b/examples/shell/src/main.rs index 54c36f5..c2d310d 100644 --- a/examples/shell/src/main.rs +++ b/examples/shell/src/main.rs @@ -1,12 +1,24 @@ -use ssh_rs::{ssh, LocalShell, SshErrorKind}; +use ssh::{self, LocalShell, SshError}; +use tracing::Level; +use tracing_subscriber::FmtSubscriber; + +use std::time::Duration; fn main() { - ssh::enable_log(); + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let mut session = ssh::create_session() .username("ubuntu") .password("password") - .timeout(1000) + .timeout(Some(Duration::from_millis(1000))) .private_key_path("./id_rsa") .connect("127.0.0.1:22") .unwrap() @@ -31,7 +43,7 @@ fn run_shell(shell: &mut LocalShell) { match shell.read() { Ok(out) => print!("{}", String::from_utf8(out).unwrap()), Err(e) => { - if let SshErrorKind::Timeout = e.kind() { + if let SshError::TimeoutError = e { break; } else { panic!("{}", e.to_string()) diff --git a/examples/shell_backend/Cargo.toml b/examples/shell_backend/Cargo.toml index 16ffffa..e867656 100644 --- a/examples/shell_backend/Cargo.toml +++ b/examples/shell_backend/Cargo.toml @@ -7,3 +7,5 @@ edition = "2021" [dependencies] ssh-rs = { path = "../../" } +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } diff --git a/examples/shell_backend/src/main.rs b/examples/shell_backend/src/main.rs index 933c827..41f5fa9 100644 --- a/examples/shell_backend/src/main.rs +++ b/examples/shell_backend/src/main.rs @@ -1,9 +1,19 @@ -use ssh_rs::{ssh, ShellBrocker}; +use ssh::{self, ShellBrocker}; use std::thread::sleep; use std::time::Duration; +use tracing::Level; +use tracing_subscriber::FmtSubscriber; fn main() { - ssh::enable_log(); + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let mut session = ssh::create_session() .username("ubuntu") diff --git a/examples/shell_interactive/Cargo.toml b/examples/shell_interactive/Cargo.toml index c0b1bd5..b7e8ac2 100644 --- a/examples/shell_interactive/Cargo.toml +++ b/examples/shell_interactive/Cargo.toml @@ -7,5 +7,7 @@ edition = "2021" [dependencies] ssh-rs = { path = "../../" } +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } # nix = "0.25.0" mio = { version="0.8.5", features = ["os-poll", "net", "os-ext"]} \ No newline at end of file diff --git a/examples/shell_interactive/src/main.rs b/examples/shell_interactive/src/main.rs index 735ff7a..84539d2 100644 --- a/examples/shell_interactive/src/main.rs +++ b/examples/shell_interactive/src/main.rs @@ -1,10 +1,13 @@ #[cfg(unix)] use mio::unix::SourceFd; -use ssh_rs::ssh; + use std::fs::File; #[cfg(unix)] use std::os::unix::io::FromRawFd; +use std::time::Duration; use std::{cell::RefCell, rc::Rc}; +use tracing::Level; +use tracing_subscriber::FmtSubscriber; use mio::net::TcpStream; use mio::{event::Source, Events, Interest, Poll, Token}; @@ -21,12 +24,20 @@ fn main() { fn main() { use std::{io::Read, os::unix::prelude::AsRawFd}; - ssh::enable_log(); + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let tcp = TcpStream::connect("127.0.0.1:22".parse().unwrap()).unwrap(); let mut session = ssh::create_session() .username("ubuntu") .password("password") - .timeout(1000) + .timeout(Some(Duration::from_millis(1000))) .private_key_path("./id_rsa") .connect_bio(tcp) .unwrap() diff --git a/examples/timeout/Cargo.toml b/examples/timeout/Cargo.toml index 9a40b20..b749571 100644 --- a/examples/timeout/Cargo.toml +++ b/examples/timeout/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ssh-rs = { path = "../../" } \ No newline at end of file +ssh-rs = { path = "../../" } +tracing = { version = "^0.1", features = ["log"] } +tracing-subscriber = { version = "^0.3" } \ No newline at end of file diff --git a/examples/timeout/src/main.rs b/examples/timeout/src/main.rs index 96e144a..42ca035 100644 --- a/examples/timeout/src/main.rs +++ b/examples/timeout/src/main.rs @@ -1,15 +1,26 @@ -use ssh_rs::ssh; + use std::net::TcpListener; +use std::time::Duration; +use tracing::Level; +use tracing_subscriber::FmtSubscriber; fn main() { - ssh::enable_log(); + // a builder for `FmtSubscriber`. + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than INFO (e.g, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::INFO) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let _listener = TcpListener::bind("127.0.0.1:7777").unwrap(); match ssh::create_session() .username("ubuntu") .password("password") .private_key_path("./id_rsa") - .timeout(5 * 1000) + .timeout(Some(Duration::from_secs(5))) .connect("127.0.0.1:7777") { Err(e) => println!("Got error {}", e), diff --git a/src/algorithm/compression/mod.rs b/src/algorithm/compression/mod.rs new file mode 100644 index 0000000..8c9e6f8 --- /dev/null +++ b/src/algorithm/compression/mod.rs @@ -0,0 +1,43 @@ +use super::Compress; +use crate::SshResult; + +mod zlib; +/// +pub(crate) trait Compression: Send + Sync { + fn new() -> Self + where + Self: Sized; + // The "zlib@openssh.com" method operates identically to the "zlib" + // method described in [RFC4252] except that packet compression does not + // start until the server sends a SSH_MSG_USERAUTH_SUCCESS packet + // so + // fn start(); + fn compress(&mut self, buf: &[u8]) -> SshResult>; + fn decompress(&mut self, buf: &[u8]) -> SshResult>; +} + +pub(crate) fn from(comp: &Compress) -> Box { + match comp { + Compress::None => Box::new(CompressNone::new()), + #[cfg(feature = "deprecated-zlib")] + Compress::Zlib => Box::new(zlib::CompressZlib::new()), + Compress::ZlibOpenSsh => Box::new(zlib::CompressZlib::new()), + } +} + +#[derive(Default)] +pub(crate) struct CompressNone {} + +impl Compression for CompressNone { + fn new() -> Self { + Self {} + } + + fn compress(&mut self, buf: &[u8]) -> SshResult> { + Ok(buf.to_vec()) + } + + fn decompress(&mut self, buf: &[u8]) -> SshResult> { + Ok(buf.to_vec()) + } +} diff --git a/src/algorithm/compression/zlib.rs b/src/algorithm/compression/zlib.rs new file mode 100644 index 0000000..6670e9f --- /dev/null +++ b/src/algorithm/compression/zlib.rs @@ -0,0 +1,118 @@ +use flate2; + +use crate::SshError; + +use super::Compression; + +/// The "zlib" compression is described in [RFC1950] and in [RFC1951]. +/// The compression context is initialized after each key exchange, and +/// is passed from one packet to the next, with only a partial flush +/// being performed at the end of each packet. A partial flush means +/// that the current compressed block is ended and all data will be +/// output. If the current block is not a stored block, one or more +/// empty blocks are added after the current block to ensure that there +/// are at least 8 bits, counting from the start of the end-of-block code +/// of the current block to the end of the packet payload. +/// +/// +/// The "zlib@openssh.com" method operates identically to the "zlib" +/// method described in [RFC4252] except that packet compression does not +/// start until the server sends a SSH_MSG_USERAUTH_SUCCESS packet, +/// replacing the "zlib" method's start of compression when the server +/// sends SSH_MSG_NEWKEYS. +pub(super) struct CompressZlib { + decompressor: flate2::Decompress, + compressor: flate2::Compress, +} + +impl Compression for CompressZlib { + fn new() -> Self + where + Self: Sized, + { + Self { + decompressor: flate2::Decompress::new(true), + compressor: flate2::Compress::new(flate2::Compression::fast(), true), + } + } + + fn decompress(&mut self, buf: &[u8]) -> crate::SshResult> { + let mut buf_in = buf; + let mut buf_once = [0; 4096]; + let mut buf_out = vec![]; + loop { + let in_before = self.decompressor.total_in(); + let out_before = self.decompressor.total_out(); + + let result = + self.decompressor + .decompress(buf_in, &mut buf_once, flate2::FlushDecompress::Sync); + + let consumed = (self.decompressor.total_in() - in_before) as usize; + let produced = (self.decompressor.total_out() - out_before) as usize; + + match result { + Ok(flate2::Status::Ok) => { + buf_in = &buf_in[consumed..]; + buf_out.extend(&buf_once[..produced]); + } + Ok(flate2::Status::StreamEnd) => { + return Err(SshError::CompressionError( + "Stream ends during the decompress".to_owned(), + )); + } + Ok(flate2::Status::BufError) => { + break; + } + Err(e) => return Err(SshError::CompressionError(e.to_string())), + } + } + + Ok(buf_out) + } + + fn compress(&mut self, buf: &[u8]) -> crate::SshResult> { + let mut buf_in = buf; + let mut buf_once = [0; 4096]; + let mut buf_out = vec![]; + loop { + let in_before = self.compressor.total_in(); + let out_before = self.compressor.total_out(); + + let result = + self.compressor + .compress(buf_in, &mut buf_once, flate2::FlushCompress::Partial); + + let consumed = (self.compressor.total_in() - in_before) as usize; + let produced = (self.compressor.total_out() - out_before) as usize; + + // tracing::info!(consumed); + // tracing::info!(produced); + + // means an empty compress + // 2 bytes ZLIB header at the start of the stream + // 4 bytes CRC checksum at the end of the stream + if produced == 6 { + break; + } + + match result { + Ok(flate2::Status::Ok) => { + buf_in = &buf_in[consumed..]; + buf_out.extend(&buf_once[..produced]); + } + Ok(flate2::Status::StreamEnd) => { + return Err(SshError::CompressionError( + "Stream ends during the compress".to_owned(), + )); + } + Ok(flate2::Status::BufError) => { + break; + } + Err(e) => return Err(SshError::CompressionError(e.to_string())), + } + } + + Ok(buf_out) + } +} diff --git a/src/algorithm/encryption/aes_cbc.rs b/src/algorithm/encryption/aes_cbc.rs new file mode 100644 index 0000000..3a9dd1d --- /dev/null +++ b/src/algorithm/encryption/aes_cbc.rs @@ -0,0 +1,153 @@ +use crate::{ + algorithm::{hash::Hash, mac::Mac}, + SshError, SshResult, +}; +use aes::{ + cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}, + Aes128, Aes192, Aes256, +}; +use cipher::generic_array::GenericArray; + +use super::Encryption; + +const CBC128_KEY_SIZE: usize = 16; +const CBC192_KEY_SIZE: usize = 24; +const CBC256_KEY_SIZE: usize = 32; +const IV_SIZE: usize = 16; +const BLOCK_SIZE: usize = 16; + +struct Extend { + // hmac + mac: Box, + ik_c_s: Vec, + ik_s_c: Vec, +} + +impl Extend { + fn from(mac: Box, ik_c_s: Vec, ik_s_c: Vec) -> Self { + Extend { + mac, + ik_c_s, + ik_s_c, + } + } +} + +macro_rules! crate_aes_cbc { + ($name: ident, $alg: ident, $key_size: expr) => { + pub(super) struct $name { + pub(super) client_key: cbc::Encryptor<$alg>, + pub(super) server_key: cbc::Decryptor<$alg>, + extend: Extend, + } + + impl Encryption for $name { + fn bsize(&self) -> usize { + BLOCK_SIZE + } + + fn iv_size(&self) -> usize { + IV_SIZE + } + + fn new(hash: Hash, mac: Box) -> Self + where + Self: Sized, + { + let (ck, sk) = hash.mix_ek($key_size); + let mut ckey = [0u8; $key_size]; + let mut skey = [0u8; $key_size]; + ckey.clone_from_slice(&ck[..$key_size]); + skey.clone_from_slice(&sk[..$key_size]); + + let mut civ = [0u8; IV_SIZE]; + let mut siv = [0u8; IV_SIZE]; + civ.clone_from_slice(&hash.iv_c_s[..IV_SIZE]); + siv.clone_from_slice(&hash.iv_s_c[..IV_SIZE]); + + let c = cbc::Encryptor::<$alg>::new(&ckey.into(), &civ.into()); + let r = cbc::Decryptor::<$alg>::new(&skey.into(), &siv.into()); + // hmac + let (ik_c_s, ik_s_c) = hash.mix_ik(mac.bsize()); + $name { + client_key: c, + server_key: r, + extend: Extend::from(mac, ik_c_s, ik_s_c), + } + } + + fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec) { + let len = buf.len(); + let tag = self + .extend + .mac + .sign(&self.extend.ik_c_s, client_sequence_num, buf); + let mut idx = 0; + while idx < len { + let mut block = GenericArray::clone_from_slice(&buf[idx..idx + BLOCK_SIZE]); + self.client_key.encrypt_block_mut(&mut block); + buf[idx..idx + BLOCK_SIZE].clone_from_slice(&block); + + idx += BLOCK_SIZE; + } + buf.extend(tag.as_ref()) + } + + fn decrypt( + &mut self, + server_sequence_number: u32, + buf: &mut [u8], + ) -> SshResult> { + let pl = self.packet_len(server_sequence_number, buf); + let data = &mut buf[..(pl + self.extend.mac.bsize())]; + let (d, m) = data.split_at_mut(pl); + + let len = d.len(); + let mut idx = 0; + while idx < len { + let mut block = GenericArray::clone_from_slice(&d[idx..idx + BLOCK_SIZE]); + self.server_key.decrypt_block_mut(&mut block); + d[idx..idx + BLOCK_SIZE].clone_from_slice(&block); + + idx += BLOCK_SIZE; + } + + let tag = self + .extend + .mac + .sign(&self.extend.ik_s_c, server_sequence_number, d); + let t = tag.as_ref(); + if m != t { + return Err(SshError::EncryptionError( + "Failed to decrypt the server traffic".to_owned(), + )); + } + Ok(d.to_vec()) + } + + fn packet_len(&mut self, _: u32, buf: &[u8]) -> usize { + let mut block = GenericArray::clone_from_slice(&buf[..BLOCK_SIZE]); + self.server_key.clone().decrypt_block_mut(&mut block); + let packet_len = u32::from_be_bytes(block[..4].try_into().unwrap()); + (packet_len + 4) as usize + } + + fn data_len(&mut self, server_sequence_number: u32, buf: &[u8]) -> usize { + let pl = self.packet_len(server_sequence_number, buf); + let bsize = self.extend.mac.bsize(); + pl + bsize + } + + fn no_pad(&self) -> bool { + false + } + } + }; +} + +// aes-128-cbc +crate_aes_cbc!(Cbc128, Aes128, CBC128_KEY_SIZE); +// aes-192-cbc +crate_aes_cbc!(Cbc192, Aes192, CBC192_KEY_SIZE); +// aes-256-cbc +crate_aes_cbc!(Cbc256, Aes256, CBC256_KEY_SIZE); diff --git a/src/algorithm/encryption/aes_ctr.rs b/src/algorithm/encryption/aes_ctr.rs index ce97891..b3fd350 100644 --- a/src/algorithm/encryption/aes_ctr.rs +++ b/src/algorithm/encryption/aes_ctr.rs @@ -3,10 +3,20 @@ use crate::algorithm::hash::Hash; use crate::algorithm::mac::Mac; use crate::error::SshError; use crate::SshResult; -use aes::cipher::{NewCipher, StreamCipher, StreamCipherSeek}; -use aes::{Aes128Ctr, Aes192Ctr, Aes256Ctr}; +use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; +use ctr; -// 拓展数据 +type Aes128Ctr64BE = ctr::Ctr64BE; +type Aes192Ctr64BE = ctr::Ctr64BE; +type Aes256Ctr64BE = ctr::Ctr64BE; + +const CTR128_KEY_SIZE: usize = 16; +const CTR192_KEY_SIZE: usize = 24; +const CTR256_KEY_SIZE: usize = 32; +const IV_SIZE: usize = 16; +const BLOCK_SIZE: usize = 16; + +// extend data for data encryption struct Extend { // hmac mac: Box, @@ -23,232 +33,107 @@ impl Extend { } } -const CTR128_BLOCK_SIZE: usize = 16; -const CTR192_BLOCK_SIZE: usize = 24; -const CTR256_BLOCK_SIZE: usize = 32; -const IV_SIZE: usize = 16; - -// aes-128-ctr -pub(crate) struct Ctr128 { - pub(crate) client_key: Aes128Ctr, - pub(crate) server_key: Aes128Ctr, - extend: Extend, -} - -impl Encryption for Ctr128 { - fn bsize(&self) -> usize { - CTR128_BLOCK_SIZE - } - - fn iv_size(&self) -> usize { - IV_SIZE - } - - fn group_size(&self) -> usize { - IV_SIZE - } - - fn new(hash: Hash, mac: Box) -> Self - where - Self: Sized, - { - crate::new!(Aes128Ctr, Ctr128, hash, mac, CTR128_BLOCK_SIZE, IV_SIZE) - } - - fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec) { - crate::encrypt!(self, client_sequence_num, buf) - } - - fn decrypt(&mut self, server_sequence_number: u32, buf: &mut [u8]) -> SshResult> { - crate::decrypt!(self, server_sequence_number, buf) - } - - fn packet_len(&mut self, _: u32, buf: &[u8]) -> usize { - crate::pl!(self, buf) - } - - fn data_len(&mut self, server_sequence_number: u32, buf: &[u8]) -> usize { - crate::dl!(self, server_sequence_number, buf) - } - - fn is_cp(&self) -> bool { - false - } -} - -// aes-192-ctr -pub(crate) struct Ctr192 { - pub(crate) client_key: Aes192Ctr, - pub(crate) server_key: Aes192Ctr, - extend: Extend, -} - -impl Encryption for Ctr192 { - fn bsize(&self) -> usize { - CTR192_BLOCK_SIZE - } - - fn iv_size(&self) -> usize { - IV_SIZE - } - - fn group_size(&self) -> usize { - IV_SIZE - } - - fn new(hash: Hash, mac: Box) -> Self - where - Self: Sized, - { - crate::new!(Aes192Ctr, Ctr192, hash, mac, CTR192_BLOCK_SIZE, IV_SIZE) - } - - fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec) { - crate::encrypt!(self, client_sequence_num, buf) - } - - fn decrypt(&mut self, server_sequence_number: u32, buf: &mut [u8]) -> SshResult> { - crate::decrypt!(self, server_sequence_number, buf) - } - - fn packet_len(&mut self, _: u32, buf: &[u8]) -> usize { - crate::pl!(self, buf) - } - - fn data_len(&mut self, server_sequence_number: u32, buf: &[u8]) -> usize { - crate::dl!(self, server_sequence_number, buf) - } - - fn is_cp(&self) -> bool { - false - } -} - -// aes-256-ctr -pub(crate) struct Ctr256 { - pub(crate) client_key: Aes256Ctr, - pub(crate) server_key: Aes256Ctr, - extend: Extend, -} - -impl Encryption for Ctr256 { - fn bsize(&self) -> usize { - CTR256_BLOCK_SIZE - } - - fn iv_size(&self) -> usize { - IV_SIZE - } - - fn group_size(&self) -> usize { - IV_SIZE - } - - fn new(hash: Hash, mac: Box) -> Self - where - Self: Sized, - { - crate::new!(Aes256Ctr, Ctr256, hash, mac, CTR256_BLOCK_SIZE, IV_SIZE) - } - - fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec) { - crate::encrypt!(self, client_sequence_num, buf) - } - - fn decrypt(&mut self, server_sequence_number: u32, buf: &mut [u8]) -> SshResult> { - crate::decrypt!(self, server_sequence_number, buf) - } - - fn packet_len(&mut self, _: u32, buf: &[u8]) -> usize { - crate::pl!(self, buf) - } - - fn data_len(&mut self, server_sequence_number: u32, buf: &[u8]) -> usize { - crate::dl!(self, server_sequence_number, buf) - } - - fn is_cp(&self) -> bool { - false - } -} - -#[macro_export] -macro_rules! new { - ($name1: ident, $name2: ident, $hash: expr, $mac: ident, $bs: expr, $is: expr) => {{ - let (ck, sk) = $hash.mix_ek($bs); - let mut ckey = [0u8; $bs]; - let mut skey = [0u8; $bs]; - ckey.clone_from_slice(&ck[..$bs]); - skey.clone_from_slice(&sk[..$bs]); - - let mut civ = [0u8; $is]; - let mut siv = [0u8; $is]; - civ.clone_from_slice(&$hash.iv_c_s[..$is]); - siv.clone_from_slice(&$hash.iv_s_c[..$is]); - - // TODO unwrap 未处理 - let c = $name1::new_from_slices(&ckey, &civ).unwrap(); - let r = $name1::new_from_slices(&skey, &siv).unwrap(); - // hmac - let (ik_c_s, ik_s_c) = $hash.mix_ik($mac.bsize()); - $name2 { - client_key: c, - server_key: r, - extend: Extend::from($mac, ik_c_s, ik_s_c), +macro_rules! crate_aes_ctr { + ($name: ident, $alg: ident, $key_size: expr) => { + pub(super) struct $name { + pub(super) client_key: $alg, + pub(super) server_key: $alg, + extend: Extend, } - }}; -} -#[macro_export] -macro_rules! encrypt { - ($self: expr, $ssb: expr, $buf: expr) => {{ - let vec = $buf.clone(); - let tag = $self - .extend - .mac - .sign(&$self.extend.ik_c_s, $ssb, vec.as_slice()); - $self.client_key.apply_keystream($buf); - $buf.extend(tag.as_ref()) - }}; -} - -#[macro_export] -macro_rules! decrypt { - ($self: expr, $ssb: expr, $buf: expr) => {{ - let pl = $self.packet_len($ssb, $buf); - let data = &mut $buf[..(pl + $self.extend.mac.bsize())]; - let (d, m) = data.split_at_mut(pl); - $self.server_key.apply_keystream(d); - let tag = $self.extend.mac.sign(&$self.extend.ik_s_c, $ssb, d); - let t = tag.as_ref(); - if m != t { - return Err(SshError::from("encryption error.")); + impl Encryption for $name { + fn bsize(&self) -> usize { + BLOCK_SIZE + } + + fn iv_size(&self) -> usize { + IV_SIZE + } + + fn new(hash: Hash, mac: Box) -> Self + where + Self: Sized, + { + let (ck, sk) = hash.mix_ek($key_size); + let mut ckey = [0u8; $key_size]; + let mut skey = [0u8; $key_size]; + ckey.clone_from_slice(&ck[..$key_size]); + skey.clone_from_slice(&sk[..$key_size]); + + let mut civ = [0u8; IV_SIZE]; + let mut siv = [0u8; IV_SIZE]; + civ.clone_from_slice(&hash.iv_c_s[..IV_SIZE]); + siv.clone_from_slice(&hash.iv_s_c[..IV_SIZE]); + + let c = $alg::new(&ckey.into(), &civ.into()); + let r = $alg::new(&skey.into(), &siv.into()); + // hmac + let (ik_c_s, ik_s_c) = hash.mix_ik(mac.bsize()); + $name { + client_key: c, + server_key: r, + extend: Extend::from(mac, ik_c_s, ik_s_c), + } + } + + fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec) { + let tag = self + .extend + .mac + .sign(&self.extend.ik_c_s, client_sequence_num, buf); + self.client_key.apply_keystream(buf); + buf.extend(tag.as_ref()) + } + + fn decrypt( + &mut self, + server_sequence_number: u32, + buf: &mut [u8], + ) -> SshResult> { + let pl = self.packet_len(server_sequence_number, buf); + let data = &mut buf[..(pl + self.extend.mac.bsize())]; + let (d, m) = data.split_at_mut(pl); + self.server_key.apply_keystream(d); + let tag = self + .extend + .mac + .sign(&self.extend.ik_s_c, server_sequence_number, d); + let t = tag.as_ref(); + if m != t { + return Err(SshError::EncryptionError( + "Failed to decrypt the server traffic".to_owned(), + )); + } + Ok(d.to_vec()) + } + + fn packet_len(&mut self, _: u32, buf: &[u8]) -> usize { + let bsize = self.bsize(); + let mut r = vec![0_u8; bsize]; + r.clone_from_slice(&buf[..bsize]); + self.server_key.apply_keystream(&mut r); + let pos: usize = self.server_key.current_pos(); + self.server_key.seek(pos - bsize); + let packet_len = u32::from_be_bytes(r[..4].try_into().unwrap()); + (packet_len + 4) as usize + } + + fn data_len(&mut self, server_sequence_number: u32, buf: &[u8]) -> usize { + let pl = self.packet_len(server_sequence_number, buf); + let bsize = self.extend.mac.bsize(); + pl + bsize + } + + fn no_pad(&self) -> bool { + false + } } - Ok(d.to_vec()) - }}; -} - -#[macro_export] -macro_rules! dl { - ($self: expr, $ssb: expr, $buf: expr) => {{ - let pl = $self.packet_len($ssb, $buf); - let bsize = $self.extend.mac.bsize(); - pl + bsize - }}; + }; } -#[macro_export] -macro_rules! pl { - ($self: expr, $buf: expr) => {{ - let bsize = $self.bsize(); - let mut r = vec![0_u8; bsize]; - r.clone_from_slice(&$buf[..bsize]); - $self.server_key.apply_keystream(&mut r); - let pos: usize = $self.server_key.current_pos(); - $self.server_key.seek(pos - bsize); - let mut u32_bytes = [0_u8; 4]; - u32_bytes.clone_from_slice(&r[..4]); - let packet_len = u32::from_be_bytes(u32_bytes); - (packet_len + 4) as usize - }}; -} +// aes-128-ctr +crate_aes_ctr!(Ctr128, Aes128Ctr64BE, CTR128_KEY_SIZE); +// aes-192-ctr +crate_aes_ctr!(Ctr192, Aes192Ctr64BE, CTR192_KEY_SIZE); +// aes-256-ctr +crate_aes_ctr!(Ctr256, Aes256Ctr64BE, CTR256_KEY_SIZE); diff --git a/src/algorithm/encryption/chacha20_poly1305_openssh.rs b/src/algorithm/encryption/chacha20_poly1305_openssh.rs index f394d42..2e0a75b 100644 --- a/src/algorithm/encryption/chacha20_poly1305_openssh.rs +++ b/src/algorithm/encryption/chacha20_poly1305_openssh.rs @@ -4,7 +4,10 @@ use crate::error::SshError; use crate::{algorithm::encryption::Encryption, error::SshResult}; use ring::aead::chacha20_poly1305_openssh::{OpeningKey, SealingKey}; -const BSIZE: usize = 64; +const KEY_SIZE: usize = 64; +const IV_SIZE: usize = 0; +const BLOCK_SIZE: usize = 0; +const MAC_SIZE: usize = 16; pub(super) struct ChaCha20Poly1305 { client_key: SealingKey, @@ -13,21 +16,17 @@ pub(super) struct ChaCha20Poly1305 { impl Encryption for ChaCha20Poly1305 { fn bsize(&self) -> usize { - 0 + BLOCK_SIZE } fn iv_size(&self) -> usize { - 0 - } - - fn group_size(&self) -> usize { - 64 + IV_SIZE } fn new(hash: Hash, _mac: Box) -> ChaCha20Poly1305 { - let (ck, sk) = hash.mix_ek(BSIZE); - let mut sealing_key = [0_u8; BSIZE]; - let mut opening_key = [0_u8; BSIZE]; + let (ck, sk) = hash.mix_ek(KEY_SIZE); + let mut sealing_key = [0_u8; KEY_SIZE]; + let mut opening_key = [0_u8; KEY_SIZE]; sealing_key.copy_from_slice(&ck); opening_key.copy_from_slice(&sk); @@ -38,7 +37,7 @@ impl Encryption for ChaCha20Poly1305 { } fn encrypt(&mut self, sequence_number: u32, buf: &mut Vec) { - let mut tag = [0_u8; 16]; + let mut tag = [0_u8; MAC_SIZE]; self.client_key .seal_in_place(sequence_number, buf, &mut tag); buf.append(&mut tag.to_vec()); @@ -53,11 +52,13 @@ impl Encryption for ChaCha20Poly1305 { .decrypt_packet_length(sequence_number, packet_len_slice); let packet_len = u32::from_be_bytes(packet_len_slice); let (buf, tag_) = buf.split_at_mut((packet_len + 4) as usize); - let mut tag = [0_u8; 16]; + let mut tag = [0_u8; MAC_SIZE]; tag.copy_from_slice(tag_); match self.server_key.open_in_place(sequence_number, buf, &tag) { Ok(result) => Ok([&packet_len_slice[..], result].concat()), - Err(_) => Err(SshError::from("encryption error.")), + Err(_) => Err(SshError::EncryptionError( + "Failed to decrypt the server traffic".to_owned(), + )), } } @@ -67,15 +68,15 @@ impl Encryption for ChaCha20Poly1305 { let packet_len_slice = self .server_key .decrypt_packet_length(sequence_number, packet_len_slice); - u32::from_be_bytes(packet_len_slice) as usize + u32::from_be_bytes(packet_len_slice) as usize + 4 } fn data_len(&mut self, sequence_number: u32, buf: &[u8]) -> usize { let packet_len = self.packet_len(sequence_number, buf); - packet_len + 4 + 16 + packet_len + MAC_SIZE } - fn is_cp(&self) -> bool { + fn no_pad(&self) -> bool { true } } diff --git a/src/algorithm/encryption/des_cbc.rs b/src/algorithm/encryption/des_cbc.rs new file mode 100644 index 0000000..2d15020 --- /dev/null +++ b/src/algorithm/encryption/des_cbc.rs @@ -0,0 +1,133 @@ +use crate::{ + algorithm::{hash::Hash, mac::Mac}, + SshError, SshResult, +}; +use cipher::{generic_array::GenericArray, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use des::TdesEde3 as des; + +use super::Encryption; + +const KEY_SIZE: usize = 24; +const IV_SIZE: usize = 8; +const BLOCK_SIZE: usize = 8; + +struct Extend { + // hmac + mac: Box, + ik_c_s: Vec, + ik_s_c: Vec, +} + +impl Extend { + fn from(mac: Box, ik_c_s: Vec, ik_s_c: Vec) -> Self { + Extend { + mac, + ik_c_s, + ik_s_c, + } + } +} + +pub(super) struct Cbc { + pub(super) client_key: cbc::Encryptor, + pub(super) server_key: cbc::Decryptor, + extend: Extend, +} + +impl Encryption for Cbc { + fn bsize(&self) -> usize { + BLOCK_SIZE + } + + fn iv_size(&self) -> usize { + IV_SIZE + } + + fn new(hash: Hash, mac: Box) -> Self + where + Self: Sized, + { + let (ck, sk) = hash.mix_ek(KEY_SIZE); + let mut ckey = [0u8; KEY_SIZE]; + let mut skey = [0u8; KEY_SIZE]; + ckey.clone_from_slice(&ck[..KEY_SIZE]); + skey.clone_from_slice(&sk[..KEY_SIZE]); + + let mut civ = [0u8; IV_SIZE]; + let mut siv = [0u8; IV_SIZE]; + civ.clone_from_slice(&hash.iv_c_s[..IV_SIZE]); + siv.clone_from_slice(&hash.iv_s_c[..IV_SIZE]); + + let c = cbc::Encryptor::::new(&ckey.into(), &civ.into()); + let r = cbc::Decryptor::::new(&skey.into(), &siv.into()); + // hmac + let (ik_c_s, ik_s_c) = hash.mix_ik(mac.bsize()); + Cbc { + client_key: c, + server_key: r, + extend: Extend::from(mac, ik_c_s, ik_s_c), + } + } + + fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec) { + let len = buf.len(); + let tag = self + .extend + .mac + .sign(&self.extend.ik_c_s, client_sequence_num, buf); + let mut idx = 0; + while idx < len { + let mut block = GenericArray::clone_from_slice(&buf[idx..idx + BLOCK_SIZE]); + self.client_key.encrypt_block_mut(&mut block); + buf[idx..idx + BLOCK_SIZE].clone_from_slice(&block); + + idx += BLOCK_SIZE; + } + buf.extend(tag.as_ref()) + } + + fn decrypt(&mut self, server_sequence_number: u32, buf: &mut [u8]) -> SshResult> { + let pl = self.packet_len(server_sequence_number, buf); + let data = &mut buf[..(pl + self.extend.mac.bsize())]; + let (d, m) = data.split_at_mut(pl); + + let len = d.len(); + let mut idx = 0; + while idx < len { + let mut block = GenericArray::clone_from_slice(&d[idx..idx + BLOCK_SIZE]); + self.server_key.decrypt_block_mut(&mut block); + d[idx..idx + BLOCK_SIZE].clone_from_slice(&block); + + idx += BLOCK_SIZE; + } + + let tag = self + .extend + .mac + .sign(&self.extend.ik_s_c, server_sequence_number, d); + let t = tag.as_ref(); + if m != t { + return Err(SshError::EncryptionError( + "Failed to decrypt the server traffic".to_owned(), + )); + } + Ok(d.to_vec()) + } + + fn packet_len(&mut self, _: u32, buf: &[u8]) -> usize { + let mut block = GenericArray::clone_from_slice(&buf[..BLOCK_SIZE]); + self.server_key.clone().decrypt_block_mut(&mut block); + let packet_len = u32::from_be_bytes(block[..4].try_into().unwrap()); + (packet_len + 4) as usize + } + + fn data_len(&mut self, server_sequence_number: u32, buf: &[u8]) -> usize { + let pl = self.packet_len(server_sequence_number, buf); + let bsize = self.extend.mac.bsize(); + pl + bsize + } + + fn no_pad(&self) -> bool { + false + } +} diff --git a/src/algorithm/encryption/mod.rs b/src/algorithm/encryption/mod.rs index 9d43770..ecdd992 100644 --- a/src/algorithm/encryption/mod.rs +++ b/src/algorithm/encryption/mod.rs @@ -1,5 +1,9 @@ +#[cfg(feature = "deprecated-aes-cbc")] +mod aes_cbc; mod aes_ctr; mod chacha20_poly1305_openssh; +#[cfg(feature = "deprecated-des-cbc")] +mod des_cbc; use crate::algorithm::hash::Hash; use crate::algorithm::mac::Mac; @@ -7,18 +11,10 @@ use crate::SshResult; use super::{hash::HashCtx, mac::MacNone, Enc}; -/// # 加密算法 -/// 在密钥交互中将协商出一种加密算法和一个密钥。当加密生效时,每个数据包的数据包长度、填 -/// 充长度、有效载荷和填充域必须使用给定的算法加密。 -/// 所有从一个方向发送的数据包中的加密数据应被认为是一个数据流。例如,初始向量应从一个数 -/// 据包的结束传递到下一个数据包的开始。所有加密器应使用有效密钥长度为 128 位或以上的密 -/// 钥。 -/// 两个方向上的加密器必须独立运行。如果本地策略允许多种算法,系统实现必须允许独立选择每 -/// 个方向上的算法。但是,在实际使用中,推荐在两个方向上使用相同的算法。 +/// pub(crate) trait Encryption: Send + Sync { fn bsize(&self) -> usize; fn iv_size(&self) -> usize; - fn group_size(&self) -> usize; fn new(hash: Hash, mac: Box) -> Self where Self: Sized; @@ -26,7 +22,7 @@ pub(crate) trait Encryption: Send + Sync { fn decrypt(&mut self, sequence_number: u32, buf: &mut [u8]) -> SshResult>; fn packet_len(&mut self, sequence_number: u32, buf: &[u8]) -> usize; fn data_len(&mut self, sequence_number: u32, buf: &[u8]) -> usize; - fn is_cp(&self) -> bool; + fn no_pad(&self) -> bool; } pub(crate) fn from(s: &Enc, hash: Hash, mac: Box) -> Box { @@ -37,6 +33,14 @@ pub(crate) fn from(s: &Enc, hash: Hash, mac: Box) -> Box Box::new(aes_ctr::Ctr128::new(hash, mac)), Enc::Aes192Ctr => Box::new(aes_ctr::Ctr192::new(hash, mac)), Enc::Aes256Ctr => Box::new(aes_ctr::Ctr256::new(hash, mac)), + #[cfg(feature = "deprecated-aes-cbc")] + Enc::Aes128Cbc => Box::new(aes_cbc::Cbc128::new(hash, mac)), + #[cfg(feature = "deprecated-aes-cbc")] + Enc::Aes192Cbc => Box::new(aes_cbc::Cbc192::new(hash, mac)), + #[cfg(feature = "deprecated-aes-cbc")] + Enc::Aes256Cbc => Box::new(aes_cbc::Cbc256::new(hash, mac)), + #[cfg(feature = "deprecated-des-cbc")] + Enc::TripleDesCbc => Box::new(des_cbc::Cbc::new(hash, mac)), } } @@ -50,10 +54,6 @@ impl Encryption for EncryptionNone { 8 } - fn group_size(&self) -> usize { - 8 - } - fn new(_hash: Hash, _mac: Box) -> Self where Self: Sized, @@ -72,7 +72,7 @@ impl Encryption for EncryptionNone { fn data_len(&mut self, sequence_number: u32, buf: &[u8]) -> usize { self.packet_len(sequence_number, buf) + 4 } - fn is_cp(&self) -> bool { + fn no_pad(&self) -> bool { false } } diff --git a/src/algorithm/hash/hash.rs b/src/algorithm/hash/hash.rs index ffad05f..a4d94e0 100644 --- a/src/algorithm/hash/hash.rs +++ b/src/algorithm/hash/hash.rs @@ -3,36 +3,65 @@ use crate::algorithm::hash; use crate::algorithm::hash::HashType; use crate::constant; -/// 加密密钥必须是对一个已知值和 K 的 HASH 结果,方法如下: -/// ○ 客户端到服务器的初始 IV:HASH(K || H || "A" || session_id)(这里 K 为 mpint -/// 格式,"A"为 byte 格式,session_id 为原始数据(raw data)。"A"是单个字母 A, -/// ASCII 65)。 -/// ○ 服务器到客户端的初始 IV:HASH(K || H || "B" || session_id) -/// ○ 客户端到服务器的加密密钥:HASH(K || H || "C" || session_id) -/// ○ 服务器到客户端的加密密钥:HASH(K || H || "D" || session_id) -/// ○ 客户端到服务器的完整性密钥:HASH(K || H || "E" || session_id) -/// ○ 服务器到客户端的完整性密钥:HASH(K || H || "F" || session_id) -/// 密钥数据必须从哈希输出的开头开始取。即从哈希值的开头开始,取所需数量的字节。如果需要 -/// 的密钥长度超过 HASH 输出,则拼接 K、H 和当前的整个密钥并计算其 HASH,然后将 HASH 产 -/// 生的字节附加到密钥尾部。重复该过程,直到获得了足够的密钥材料;密钥从该值的开头开始取 -/// 换句话说: -/// K1 = HASH(K || H || X || session_id)(X 表示"A"等) -/// K2 = HASH(K || H || K1) -/// K3 = HASH(K || H || K1 || K2) -/// ... -/// key = K1 || K2 || K3 || ... -/// 如果 K 的熵比 HASH 的内状态(internal state)大小要大,则该过程将造成熵的丢失。 +/// +/// +/// The key exchange produces two values: a shared secret K, and an +/// exchange hash H. Encryption and authentication keys are derived from +/// these. The exchange hash H from the first key exchange is +/// additionally used as the session identifier, which is a unique +/// identifier for this connection. It is used by authentication methods +/// as a part of the data that is signed as a proof of possession of a +/// private key. Once computed, the session identifier is not changed, +/// even if keys are later re-exchanged. +/// Each key exchange method specifies a hash function that is used in +/// the key exchange. The same hash algorithm MUST be used in key +/// derivation. Here, we'll call it HASH. + +/// Encryption keys MUST be computed as HASH, of a known value and K, as +/// follows: + +/// o Initial IV client to server: HASH(K || H || "A" || session_id) +/// (Here K is encoded as mpint and "A" as byte and session_id as raw +/// data. "A" means the single character A, ASCII 65). + +/// o Initial IV server to client: HASH(K || H || "B" || session_id) + +/// o Encryption key client to server: HASH(K || H || "C" || session_id) + +/// o Encryption key server to client: HASH(K || H || "D" || session_id) + +/// o Integrity key client to server: HASH(K || H || "E" || session_id) + +/// o Integrity key server to client: HASH(K || H || "F" || session_id) + +/// Key data MUST be taken from the beginning of the hash output. As +/// many bytes as needed are taken from the beginning of the hash value. +/// If the key length needed is longer than the output of the HASH, the +/// key is extended by computing HASH of the concatenation of K and H and +/// the entire key so far, and appending the resulting bytes (as many as +/// HASH generates) to the key. This process is repeated until enough +/// key material is available; the key is taken from the beginning of +/// this value. In other words: + +/// K1 = HASH(K || H || X || session_id) (X is e.g., "A") +/// K2 = HASH(K || H || K1) +/// K3 = HASH(K || H || K1 || K2) +/// ... +/// key = K1 || K2 || K3 || ... + +/// This process will lose entropy if the amount of entropy in K is +/// larger than the internal state size of HASH. pub struct Hash { - /// 数据加密时只使用一次的随机数 number used once + /// reandom number used once pub iv_c_s: Vec, pub iv_s_c: Vec, - /// 数据加密的 key + /// key used for data exchange pub ek_c_s: Vec, pub ek_s_c: Vec, - /// Hmac时候用到的 key + /// key used for hmac pub ik_c_s: Vec, pub ik_s_c: Vec, diff --git a/src/algorithm/hash/hash_ctx.rs b/src/algorithm/hash/hash_ctx.rs index d8e3ddc..7337a61 100644 --- a/src/algorithm/hash/hash_ctx.rs +++ b/src/algorithm/hash/hash_ctx.rs @@ -1,48 +1,40 @@ use crate::model::Data; -/// 密钥交换产生两个值:一个共享秘密 K,以及一个交换哈希 H。加密和验证密钥来自它们。第一 -/// 次密钥交换的交换哈希 H 也被用作会话标识,它是一个对该连接的唯一标识。它是验证方法中 -/// 被签名(以证明拥有私钥)的数据的一部分。会话标识被计算出来后,即使后来重新交换了密钥, -/// 也不会改变。 +/// /// +/// The key exchange produces two values: a shared secret K, and an +/// exchange hash H. Encryption and authentication keys are derived from +/// these. The exchange hash H from the first key exchange is +/// additionally used as the session identifier, which is a unique +/// identifier for this connection. It is used by authentication methods +/// as a part of the data that is signed as a proof of possession of a +/// private key. Once computed, the session identifier is not changed, +/// even if keys are later re-exchanged. /// -/// H = hash algorithm(v_c | v_s | i_c | i_s | k_s | q_c | q_s | k) +/// H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K) /// /// #[derive(Clone, Debug, Default)] pub struct HashCtx { - /// 一下数据如果有从数据包解析的数据 - /// 统一不包含数据包里面的 PacketLength PaddingLength PaddingString - - /// 数据统一转为 [bytes] - - /// 双方(客户端/服务端)的版本, - /// 数据长度 + 数据, 不包含 /r/n - /// 数据长度 [u32] - /// 数据 [str] + /// string V_C, the client's identification string (CR and LF excluded) pub v_c: Vec, + /// string V_S, the server's identification string (CR and LF excluded) pub v_s: Vec, - /// 双方(客户端/服务端)交换的算法, - /// 数据长度 + 数据 - /// 数据长度 [u32] - /// 数据(客户端密钥交换信息数组) [`[str, str ...]`] + /// string I_C, the payload of the client's SSH_MSG_KEXINIT pub i_c: Vec, + /// string I_S, the payload of the server's SSH_MSG_KEXINIT pub i_s: Vec, - /// 主机密钥 - /// 服务端发过来的host key 整体数据 + /// string K_S, the host key pub k_s: Vec, - /// 双方(客户端/服务端)的公钥, - /// 数据长度 + 数据 - /// 数据长度 [u32] - /// 数据(客户端密钥交换信息数组) [bytes] - pub q_c: Vec, - pub q_s: Vec, + /// mpint e, exchange value sent by the client + pub e: Vec, + /// mpint f, exchange value sent by the server + pub f: Vec, - /// 共享密钥 - /// 二进制补码 + 数据 + /// mpint K, the shared secret pub k: Vec, } @@ -54,8 +46,8 @@ impl HashCtx { i_c: vec![], i_s: vec![], k_s: vec![], - q_c: vec![], - q_s: vec![], + e: vec![], + f: vec![], k: vec![], } } @@ -85,15 +77,15 @@ impl HashCtx { data.put_u8s(ks); self.k_s = data.to_vec(); } - pub fn set_q_c(&mut self, qc: &[u8]) { + pub fn set_e(&mut self, qc: &[u8]) { let mut data = Data::new(); data.put_u8s(qc); - self.q_c = data.to_vec(); + self.e = data.to_vec(); } - pub fn set_q_s(&mut self, qs: &[u8]) { + pub fn set_f(&mut self, qs: &[u8]) { let mut data = Data::new(); data.put_u8s(qs); - self.q_s = data.to_vec(); + self.f = data.to_vec(); } pub fn set_k(&mut self, k: &[u8]) { let mut data = Data::new(); @@ -108,8 +100,8 @@ impl HashCtx { v.extend(&self.i_c); v.extend(&self.i_s); v.extend(&self.k_s); - v.extend(&self.q_c); - v.extend(&self.q_s); + v.extend(&self.e); + v.extend(&self.f); v.extend(&self.k); v } diff --git a/src/algorithm/hash/hash_type.rs b/src/algorithm/hash/hash_type.rs index aa654ec..eff485e 100644 --- a/src/algorithm/hash/hash_type.rs +++ b/src/algorithm/hash/hash_type.rs @@ -1,4 +1,5 @@ -/// 密钥交换对应的hash算法 +/// The hash type used during kex +/// this is determined by the kex alg #[derive(Copy, Clone)] pub enum HashType { None, diff --git a/src/algorithm/key_exchange/curve25519.rs b/src/algorithm/key_exchange/curve25519.rs index ba6a498..cd4e5f9 100644 --- a/src/algorithm/key_exchange/curve25519.rs +++ b/src/algorithm/key_exchange/curve25519.rs @@ -13,14 +13,14 @@ impl KeyExchange for CURVE25519 { let rng = ring::rand::SystemRandom::new(); let private_key = match EphemeralPrivateKey::generate(&X25519, &rng) { Ok(v) => v, - Err(_) => return Err(SshError::from("encryption error.")), + Err(e) => return Err(SshError::KexError(e.to_string())), }; match private_key.compute_public_key() { Ok(public_key) => Ok(CURVE25519 { private_key, public_key, }), - Err(_) => Err(SshError::from("encryption error.")), + Err(e) => Err(SshError::KexError(e.to_string())), } } diff --git a/src/algorithm/key_exchange/dh.rs b/src/algorithm/key_exchange/dh.rs index c131a30..be49b32 100644 --- a/src/algorithm/key_exchange/dh.rs +++ b/src/algorithm/key_exchange/dh.rs @@ -5,7 +5,7 @@ use crate::SshResult; use super::{HashType, KeyExchange}; -#[cfg(feature = "dangerous-dh-group1-sha1")] +#[cfg(feature = "deprecated-dh-group1-sha1")] const GROUP1: [u8; 128] = [ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0xf, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x2, 0x4e, 0x8, 0x8a, 0x67, 0xcc, 0x74, @@ -42,7 +42,7 @@ struct DhGroup { exp_size: u64, } -#[cfg(feature = "dangerous-dh-group1-sha1")] +#[cfg(feature = "deprecated-dh-group1-sha1")] const DH_GROUP1: DhGroup = DhGroup { prime: &GROUP1, generator: 2, @@ -108,7 +108,7 @@ macro_rules! create_dh_with_group { }; } -#[cfg(feature = "dangerous-dh-group1-sha1")] +#[cfg(feature = "deprecated-dh-group1-sha1")] create_dh_with_group!(DiffieHellmanGroup1Sha1, DH_GROUP1, HashType::SHA1); create_dh_with_group!(DiffieHellmanGroup14Sha1, DH_GROUP14, HashType::SHA1); create_dh_with_group!(DiffieHellmanGroup14Sha256, DH_GROUP14, HashType::SHA256); diff --git a/src/algorithm/key_exchange/ecdh_sha2_nistp256.rs b/src/algorithm/key_exchange/ecdh_sha2_nistp256.rs index c357063..9315037 100644 --- a/src/algorithm/key_exchange/ecdh_sha2_nistp256.rs +++ b/src/algorithm/key_exchange/ecdh_sha2_nistp256.rs @@ -13,14 +13,14 @@ impl KeyExchange for EcdhP256 { let rng = ring::rand::SystemRandom::new(); let private_key = match EphemeralPrivateKey::generate(&ECDH_P256, &rng) { Ok(v) => v, - Err(_) => return Err(SshError::from("encryption error.")), + Err(e) => return Err(SshError::KexError(e.to_string())), }; match private_key.compute_public_key() { Ok(public_key) => Ok(EcdhP256 { private_key, public_key, }), - Err(_) => Err(SshError::from("encryption error.")), + Err(e) => Err(SshError::KexError(e.to_string())), } } diff --git a/src/algorithm/key_exchange/mod.rs b/src/algorithm/key_exchange/mod.rs index 74ac872..d860edc 100644 --- a/src/algorithm/key_exchange/mod.rs +++ b/src/algorithm/key_exchange/mod.rs @@ -3,17 +3,16 @@ use crate::{SshError, SshResult}; use ring::agreement; use ring::agreement::{EphemeralPrivateKey, UnparsedPublicKey}; -/// # 密钥交换方法 -/// -/// 密钥交换方法规定如何生成用于加密和验证的一次性会话密钥,以及如何进行服务器验证。 +/// # Algorithms that used for key exchange /// +/// mod curve25519; mod dh; mod ecdh_sha2_nistp256; use super::Kex; use curve25519::CURVE25519; -#[cfg(feature = "dangerous-dh-group1-sha1")] +#[cfg(feature = "deprecated-dh-group1-sha1")] use dh::DiffieHellmanGroup1Sha1; use dh::{DiffieHellmanGroup14Sha1, DiffieHellmanGroup14Sha256}; use ecdh_sha2_nistp256::EcdhP256; @@ -31,14 +30,11 @@ pub(crate) fn agree_ephemeral>( private_key: EphemeralPrivateKey, peer_public_key: &UnparsedPublicKey, ) -> SshResult> { - match agreement::agree_ephemeral( - private_key, - peer_public_key, - ring::error::Unspecified, - |key_material| Ok(key_material.to_vec()), - ) { - Ok(o) => Ok(o), - Err(_) => Err(SshError::from("encryption error.")), + match agreement::agree_ephemeral(private_key, peer_public_key, |key_material| { + Ok(key_material.to_vec()) + }) { + Ok(o) => o, + Err(e) => Err(SshError::KexError(e.to_string())), } } @@ -46,7 +42,7 @@ pub(crate) fn from(s: &Kex) -> SshResult> { match s { Kex::Curve25519Sha256 => Ok(Box::new(CURVE25519::new()?)), Kex::EcdhSha2Nistrp256 => Ok(Box::new(EcdhP256::new()?)), - #[cfg(feature = "dangerous-dh-group1-sha1")] + #[cfg(feature = "deprecated-dh-group1-sha1")] Kex::DiffieHellmanGroup1Sha1 => Ok(Box::new(DiffieHellmanGroup1Sha1::new()?)), Kex::DiffieHellmanGroup14Sha1 => Ok(Box::new(DiffieHellmanGroup14Sha1::new()?)), Kex::DiffieHellmanGroup14Sha256 => Ok(Box::new(DiffieHellmanGroup14Sha256::new()?)), diff --git a/src/algorithm/mod.rs b/src/algorithm/mod.rs index faaad8e..566c2e0 100644 --- a/src/algorithm/mod.rs +++ b/src/algorithm/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod compression; pub(crate) mod encryption; pub(crate) mod hash; pub(crate) mod key_exchange; @@ -19,6 +20,18 @@ pub enum Enc { Aes192Ctr, #[strum(serialize = "aes256-ctr")] Aes256Ctr, + #[cfg(feature = "deprecated-aes-cbc")] + #[strum(serialize = "aes128-cbc")] + Aes128Cbc, + #[cfg(feature = "deprecated-aes-cbc")] + #[strum(serialize = "aes192-cbc")] + Aes192Cbc, + #[cfg(feature = "deprecated-aes-cbc")] + #[strum(serialize = "aes256-cbc")] + Aes256Cbc, + #[cfg(feature = "deprecated-des-cbc")] + #[strum(serialize = "3des-cbc")] + TripleDesCbc, } /// key exchange algorithm @@ -28,7 +41,7 @@ pub enum Kex { Curve25519Sha256, #[strum(serialize = "ecdh-sha2-nistp256")] EcdhSha2Nistrp256, - #[cfg(feature = "dangerous-dh-group1-sha1")] + #[cfg(feature = "deprecated-dh-group1-sha1")] #[strum(serialize = "diffie-hellman-group1-sha1")] DiffieHellmanGroup1Sha1, #[strum(serialize = "diffie-hellman-group14-sha1")] @@ -42,13 +55,16 @@ pub enum Kex { pub enum PubKey { #[strum(serialize = "ssh-ed25519")] SshEd25519, - #[cfg(feature = "dangerous-rsa-sha1")] + #[cfg(feature = "deprecated-rsa-sha1")] #[strum(serialize = "ssh-rsa")] SshRsa, #[strum(serialize = "rsa-sha2-256")] RsaSha2_256, #[strum(serialize = "rsa-sha2-512")] RsaSha2_512, + #[cfg(feature = "deprecated-dss-sha1")] + #[strum(serialize = "ssh-dss")] + SshDss, } /// MAC(message authentication code) algorithm @@ -67,6 +83,11 @@ pub enum Mac { pub enum Compress { #[strum(serialize = "none")] None, + #[cfg(feature = "deprecated-zlib")] + #[strum(serialize = "zlib")] + Zlib, + #[strum(serialize = "zlib@openssh.com")] + ZlibOpenSsh, } #[derive(Default)] diff --git a/src/algorithm/public_key/dss.rs b/src/algorithm/public_key/dss.rs new file mode 100644 index 0000000..f967f8e --- /dev/null +++ b/src/algorithm/public_key/dss.rs @@ -0,0 +1,53 @@ +#[cfg(feature = "deprecated-dss-sha1")] +use sha1::{Digest, Sha1}; +use signature::DigestVerifier; + +use crate::algorithm::public_key::PublicKey as PubK; +use crate::model::Data; +use crate::SshError; + +#[cfg(feature = "deprecated-dss-sha1")] +pub(super) struct DssSha1; + +#[cfg(feature = "deprecated-dss-sha1")] +impl PubK for DssSha1 { + fn new() -> Self + where + Self: Sized, + { + Self + } + + fn verify_signature(&self, ks: &[u8], message: &[u8], sig: &[u8]) -> Result { + let mut data = Data::from(ks[4..].to_vec()); + data.get_u8s(); + + // RFC4253 6.6 DSS Signature key blob are 4x mpint's that need to be pulled out to be used as components in the public key. + let p = dsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); + let q = dsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); + let g = dsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); + let y = dsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); + + let components = dsa::Components::from_components(p, q, g).map_err(|_| { + SshError::SshPubKeyError("SSH Public Key components were not valid".to_string()) + })?; + + // Build the public key for verification of the message + let public_key = dsa::VerifyingKey::from_components(components, y).map_err(|_| { + SshError::SshPubKeyError("SSH Public Key components were not valid".to_string()) + })?; + + // Perform an SHA1 hash on the message + let digest = Sha1::new().chain_update(message); + + // RFC4253 6.6 DSS Signature blob is actually 2x160bit blobs so r and s are each 160bit (20 bytes) + let r = dsa::BigUint::from_bytes_be(&sig[0..20]); + let s = dsa::BigUint::from_bytes_be(&sig[20..40]); + + let signature = dsa::Signature::from_components(r, s) + .map_err(|_| SshError::SshPubKeyError("SSH Signature was not valid".to_string()))?; + + // Verify the hashed message with the provided signature, matches the public_key + Ok(public_key.verify_digest(digest, &signature).is_ok()) + } +} diff --git a/src/algorithm/public_key/mod.rs b/src/algorithm/public_key/mod.rs index f67e17b..485f40c 100644 --- a/src/algorithm/public_key/mod.rs +++ b/src/algorithm/public_key/mod.rs @@ -1,17 +1,22 @@ use crate::SshError; +#[cfg(feature = "deprecated-dss-sha1")] +mod dss; mod ed25519; mod rsa; -#[cfg(feature = "dangerous-rsa-sha1")] +#[cfg(feature = "deprecated-dss-sha1")] +use self::dss::DssSha1; +#[cfg(feature = "deprecated-rsa-sha1")] use self::rsa::RsaSha1; use self::rsa::RsaSha256; use self::rsa::RsaSha512; use super::PubKey; use ed25519::Ed25519; -/// # 公钥算法 -/// 主要用于对服务端签名的验证 +/// # Public Key Algorithms +/// +/// pub(crate) trait PublicKey: Send + Sync { fn new() -> Self @@ -23,9 +28,11 @@ pub(crate) trait PublicKey: Send + Sync { pub(crate) fn from(s: &PubKey) -> Box { match s { PubKey::SshEd25519 => Box::new(Ed25519::new()), - #[cfg(feature = "dangerous-rsa-sha1")] + #[cfg(feature = "deprecated-rsa-sha1")] PubKey::SshRsa => Box::new(RsaSha1::new()), PubKey::RsaSha2_256 => Box::new(RsaSha256::new()), PubKey::RsaSha2_512 => Box::new(RsaSha512::new()), + #[cfg(feature = "deprecated-dss-sha1")] + PubKey::SshDss => Box::new(DssSha1::new()), } } diff --git a/src/algorithm/public_key/rsa.rs b/src/algorithm/public_key/rsa.rs index 9f12305..06ebfc1 100644 --- a/src/algorithm/public_key/rsa.rs +++ b/src/algorithm/public_key/rsa.rs @@ -1,7 +1,8 @@ use crate::algorithm::public_key::PublicKey as PubK; use crate::model::Data; use crate::SshError; -use rsa::PublicKey; +//use rsa::PublicKey; +use rsa::pkcs1v15::Pkcs1v15Sign; pub(super) struct RsaSha256; @@ -20,7 +21,7 @@ impl PubK for RsaSha256 { let e = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); let n = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); let public_key = rsa::RsaPublicKey::new(n, e).unwrap(); - let scheme = rsa::PaddingScheme::new_pkcs1v15_sign::(); + let scheme = Pkcs1v15Sign::new::(); let digest = ring::digest::digest(&ring::digest::SHA256, message); let msg = digest.as_ref(); @@ -46,7 +47,7 @@ impl PubK for RsaSha512 { let e = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); let n = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); let public_key = rsa::RsaPublicKey::new(n, e).unwrap(); - let scheme = rsa::PaddingScheme::new_pkcs1v15_sign::(); + let scheme = Pkcs1v15Sign::new::(); let digest = ring::digest::digest(&ring::digest::SHA512, message); let msg = digest.as_ref(); @@ -55,9 +56,9 @@ impl PubK for RsaSha512 { } } -#[cfg(feature = "dangerous-rsa-sha1")] +#[cfg(feature = "deprecated-rsa-sha1")] pub(super) struct RsaSha1; -#[cfg(feature = "dangerous-rsa-sha1")] +#[cfg(feature = "deprecated-rsa-sha1")] impl PubK for RsaSha1 { fn new() -> Self where @@ -73,7 +74,7 @@ impl PubK for RsaSha1 { let e = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); let n = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); let public_key = rsa::RsaPublicKey::new(n, e).unwrap(); - let scheme = rsa::PaddingScheme::new_pkcs1v15_sign::(); + let scheme = Pkcs1v15Sign::new::(); let digest = ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, message); let msg = digest.as_ref(); diff --git a/src/channel/backend/channel.rs b/src/channel/backend/channel.rs index 3c3ed51..6e94878 100644 --- a/src/channel/backend/channel.rs +++ b/src/channel/backend/channel.rs @@ -6,13 +6,16 @@ use std::{ use crate::{ client::Client, - constant::ssh_msg_code, + constant::ssh_connection_code, error::{SshError, SshResult}, model::{BackendResp, BackendRqst, Data, FlowControl, Packet}, TerminalSize, }; +use tracing::*; -use super::{channel_exec::ExecBroker, channel_scp::ScpBroker, channel_shell::ShellBrocker}; +#[cfg(feature = "scp")] +use super::channel_scp::ScpBroker; +use super::{channel_exec::ExecBroker, channel_shell::ShellBrocker}; pub(crate) struct Channel { snd: Sender, @@ -63,7 +66,7 @@ impl Channel { // send it let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_DATA) + data.put_u8(ssh_connection_code::CHANNEL_DATA) .put_u32(self.server_channel_no) .put_u8s(&self.pending_send); @@ -85,7 +88,9 @@ impl Channel { if !self.is_close() { data.pack(client).write_stream(stream) } else { - Err(SshError::from("Send data on a closed channel")) + Err(SshError::GeneralError( + "Send data on a closed channel".to_owned(), + )) } } @@ -111,7 +116,7 @@ impl Channel { S: Read + Write, { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_WINDOW_ADJUST) + data.put_u8(ssh_connection_code::CHANNEL_WINDOW_ADJUST) .put_u32(self.server_channel_no) .put_u32(to_add); self.flow_control.on_send(to_add); @@ -136,13 +141,13 @@ impl Channel { } pub fn local_close(&mut self) -> SshResult<()> { - log::trace!("Channel {} send local close", self.client_channel_no); + trace!("Channel {} send local close", self.client_channel_no); self.local_close = true; Ok(()) } pub fn remote_close(&mut self) -> SshResult<()> { - log::trace!("Channel {} recv remote close", self.client_channel_no); + trace!("Channel {} recv remote close", self.client_channel_no); self.remote_close = true; if !self.local_close { self.snd.send(BackendResp::Close)?; @@ -167,7 +172,7 @@ impl Channel { impl Drop for Channel { fn drop(&mut self) { - log::info!("Channel {} closed", self.client_channel_no); + info!("Channel {} closed", self.client_channel_no); } } @@ -203,6 +208,7 @@ impl ChannelBroker { /// open a [ScpBroker] channel which can download/upload files/directories /// + #[cfg(feature = "scp")] pub fn scp(self) -> SshResult { Ok(ScpBroker::open(self)) } @@ -222,7 +228,7 @@ impl ChannelBroker { fn close_no_consue(&mut self) -> SshResult<()> { if !self.close { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_CLOSE) + data.put_u8(ssh_connection_code::CHANNEL_CLOSE) .put_u32(self.server_channel_no); self.close = true; self.snd @@ -242,11 +248,10 @@ impl ChannelBroker { .send(BackendRqst::Command(self.client_channel_no, data))?; if !self.close { match self.rcv.recv().unwrap() { - BackendResp::Ok(_) => log::trace!("{}: control command ok", self.client_channel_no), - BackendResp::Fail(msg) => log::error!( + BackendResp::Ok(_) => trace!("{}: control command ok", self.client_channel_no), + BackendResp::Fail(msg) => error!( "{}: channel error with reason {}", - self.client_channel_no, - msg + self.client_channel_no, msg ), _ => unreachable!(), } @@ -290,7 +295,9 @@ impl ChannelBroker { Ok(None) } } else { - Err(SshError::from("Read data on a closed channel")) + Err(SshError::GeneralError( + "Read data on a closed channel".to_owned(), + )) } } diff --git a/src/channel/backend/channel_exec.rs b/src/channel/backend/channel_exec.rs index f61b56f..69a62ee 100644 --- a/src/channel/backend/channel_exec.rs +++ b/src/channel/backend/channel_exec.rs @@ -1,5 +1,5 @@ use super::channel::ChannelBroker; -use crate::constant::{ssh_msg_code, ssh_str}; +use crate::constant::{ssh_connection_code, ssh_str}; use crate::error::SshResult; use crate::model::Data; use std::ops::{Deref, DerefMut}; @@ -16,9 +16,9 @@ impl ExecBroker { /// This method is non-block as it will not wait the result /// pub fn send_command(&self, command: &str) -> SshResult<()> { - log::debug!("Send command {}", command); + tracing::debug!("Send command {}", command); let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_REQUEST) + data.put_u8(ssh_connection_code::CHANNEL_REQUEST) .put_u32(self.server_channel_no) .put_str(ssh_str::EXEC) .put_u8(true as u8) diff --git a/src/channel/backend/channel_scp.rs b/src/channel/backend/channel_scp.rs index a01108b..85466c7 100644 --- a/src/channel/backend/channel_scp.rs +++ b/src/channel/backend/channel_scp.rs @@ -6,7 +6,7 @@ use crate::{ util::file_time, }; use crate::{ - constant::{ssh_msg_code, ssh_str}, + constant::{ssh_connection_code, ssh_str}, util, }; use crate::{model::Data, util::check_path}; @@ -16,8 +16,10 @@ use std::{ io::{Read, Write}, ops::{Deref, DerefMut}, path::Path, + str::FromStr, time::SystemTime, }; +use tracing::*; pub struct ScpBroker(ChannelBroker, Option); @@ -28,7 +30,7 @@ impl ScpBroker { fn exec_scp(&mut self, command: &str) -> SshResult<()> { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_REQUEST) + data.put_u8(ssh_connection_code::CHANNEL_REQUEST) .put_u32(self.server_channel_no) .put_str(ssh_str::EXEC) .put_u8(true as u8) @@ -55,13 +57,13 @@ impl ScpBroker { fn get_end(&mut self) -> SshResult<()> { let vec = self.recv()?; if vec.is_empty() { - Err(SshError::from("read a closed channel")) + Err(SshError::ScpError("read a closed channel".to_owned())) } else { match vec[0] { scp::END => Ok(()), // error - scp::ERR | scp::FATAL_ERR => Err(SshError::from(util::from_utf8(vec)?)), - _ => Err(SshError::from("unknown error.")), + scp::ERR | scp::FATAL_ERR => Err(SshError::ScpError(String::from_utf8(vec)?)), + _ => Err(SshError::ScpError("unknown error.".to_owned())), } } } @@ -95,11 +97,10 @@ impl ScpBroker { let remote_path_str = remote_path.to_str().unwrap(); let local_path_str = local_path.to_str().unwrap(); - log::info!( + info!( "start to upload files, \ local [{}] files will be synchronized to the remote [{}] folder.", - local_path_str, - remote_path_str + local_path_str, remote_path_str ); self.exec_scp(self.command_init(remote_path_str, scp::SINK).as_str())?; @@ -108,27 +109,25 @@ impl ScpBroker { scp_file.local_path = local_path.to_path_buf(); self.file_all(&mut scp_file)?; - log::info!("files upload successful."); + info!("files upload successful."); self.0.close() } fn file_all(&mut self, scp_file: &mut ScpFile) -> SshResult<()> { - // 如果获取不到文件或者目录名的话,就不处理该数据 - // 如果文件不是有效的Unicode数据的话,也不处理 + // test if input file path valid scp_file.name = match scp_file.local_path.file_name() { None => return Ok(()), Some(name) => match name.to_str() { None => return Ok(()), - Some(name) => name.to_string(), + Some(name) => name.to_owned(), }, }; self.send_time(scp_file)?; if scp_file.local_path.is_dir() { - // 文件夹如果读取异常的话。就略过该文件夹 - // 详细的错误信息请查看 [std::fs::read_dir] 方法介绍 + // skip the read_dir errs if let Err(e) = fs::read_dir(scp_file.local_path.as_path()) { - log::error!("read dir error, error info: {}", e); + error!("read dir error, error info: {}", e); return Ok(()); } self.send_dir(scp_file)?; @@ -139,13 +138,13 @@ impl ScpBroker { self.file_all(scp_file)? } Err(e) => { - // 暂不处理 - log::error!("dir entry error, error info: {}", e); + // TODO + error!("dir entry error, error info: {}", e); } } } - self.send_bytes(&[scp::E as u8, b'\n'])?; + self.send_bytes(&[scp::E, b'\n'])?; self.get_end()?; } else { scp_file.size = scp_file.local_path.as_path().metadata()?.len(); @@ -154,12 +153,11 @@ impl ScpBroker { Ok(()) } - fn send_file(&mut self, scp_file: &mut ScpFile) -> SshResult<()> { + fn send_file(&mut self, scp_file: &ScpFile) -> SshResult<()> { let mut file = match File::open(scp_file.local_path.as_path()) { Ok(f) => f, - // 文件打开异常,不影响后续操作 Err(e) => { - log::error!( + error!( "failed to open the folder, \ it is possible that the path does not exist, \ which does not affect subsequent operations. \ @@ -170,10 +168,9 @@ impl ScpBroker { } }; - log::debug!( + debug!( "name: [{}] size: [{}] type: [file] start upload.", - scp_file.name, - scp_file.size + scp_file.name, scp_file.size ); let cmd = format!( @@ -199,13 +196,13 @@ impl ScpBroker { } self.get_end()?; - log::debug!("file: [{}] upload completed.", scp_file.name); + debug!("file: [{}] upload completed.", scp_file.name); Ok(()) } fn send_dir(&mut self, scp_file: &ScpFile) -> SshResult<()> { - log::debug!( + debug!( "name: [{}] size: [0], type: [dir] start upload.", scp_file.name ); @@ -214,7 +211,7 @@ impl ScpBroker { self.send_bytes(cmd.as_bytes())?; self.get_end()?; - log::debug!("dir: [{}] upload completed.", scp_file.name); + debug!("dir: [{}] upload completed.", scp_file.name); Ok(()) } @@ -228,14 +225,12 @@ impl ScpBroker { fn get_time(&self, scp_file: &mut ScpFile) -> SshResult<()> { let metadata = scp_file.local_path.as_path().metadata()?; - // 最后修改时间 let modified_time = match metadata.modified() { Ok(t) => t, Err(_) => SystemTime::now(), }; let modified_time = util::sys_time_to_secs(modified_time)?; - // 最后访问时间 let accessed_time = match metadata.accessed() { Ok(t) => t, Err(_) => SystemTime::now(), @@ -274,11 +269,10 @@ impl ScpBroker { let local_path_str = local_path.to_str().unwrap(); let remote_path_str = remote_path.to_str().unwrap(); - log::info!( + info!( "start to download files, \ remote [{}] files will be synchronized to the local [{}] folder.", - remote_path_str, - local_path_str + remote_path_str, local_path_str ); self.exec_scp(self.command_init(remote_path_str, scp::SOURCE).as_str())?; @@ -308,7 +302,6 @@ impl ScpBroker { let code = &data[0]; match *code { scp::T => { - // 处理时间 let (modify_time, access_time) = file_time(data)?; scp_file.modify_time = modify_time; scp_file.access_time = access_time; @@ -325,15 +318,17 @@ impl ScpBroker { } }, // error - scp::ERR | scp::FATAL_ERR => return Err(SshError::from(util::from_utf8(data)?)), - _ => return Err(SshError::from("unknown error.")), + scp::ERR | scp::FATAL_ERR => { + return Err(SshError::ScpError(String::from_utf8(data)?)) + } + _ => return Err(SshError::ScpError("unknown error.".to_owned())), } } Ok(()) } fn process_dir_d(&mut self, data: Vec, scp_file: &mut ScpFile) -> SshResult<()> { - let string = util::from_utf8(data)?; + let string = String::from_utf8(data)?; let dir_info = string.trim(); let split = dir_info.split(' ').collect::>(); match split.get(2) { @@ -342,7 +337,7 @@ impl ScpBroker { } scp_file.is_dir = true; let buf = scp_file.join(&scp_file.name); - log::debug!( + debug!( "name: [{}] size: [0], type: [dir] start download.", scp_file.name ); @@ -362,29 +357,23 @@ impl ScpBroker { self.sync_permissions(scp_file, file); } Err(e) => { - log::error!( - "failed to open the folder, \ - it is possible that the path does not exist, \ - which does not affect subsequent operations. \ - error info: {:?}, path: {:?}", - e, - scp_file.local_path.to_str() - ); - return Err(SshError::from(format!("file open error: {}", e))); + let err_msg = format!("file open error: {}", e); + error!(err_msg); + return Err(SshError::ScpError(err_msg)); } }; } - log::debug!("dir: [{}] download completed.", scp_file.name); + debug!("dir: [{}] download completed.", scp_file.name); Ok(()) } fn process_file_d(&mut self, data: Vec, scp_file: &mut ScpFile) -> SshResult<()> { - let string = util::from_utf8(data)?; + let string = String::from_utf8(data)?; let file_info = string.trim(); let split = file_info.split(' ').collect::>(); let size_str = *split.get(1).unwrap_or(&"0"); - let size = util::str_to_i64(size_str)?; + let size = i64::from_str(size_str)?; scp_file.size = size as u64; match split.get(2) { None => return Ok(()), @@ -394,11 +383,10 @@ impl ScpBroker { self.save_file(scp_file) } - fn save_file(&mut self, scp_file: &mut ScpFile) -> SshResult<()> { - log::debug!( + fn save_file(&mut self, scp_file: &ScpFile) -> SshResult<()> { + debug!( "name: [{}] size: [{}] type: [file] start download.", - scp_file.name, - scp_file.size + scp_file.name, scp_file.size ); let path = scp_file.join(&scp_file.name); if path.exists() { @@ -412,11 +400,9 @@ impl ScpBroker { { Ok(v) => v, Err(e) => { - log::error!("file processing error, error info: {}", e); - return Err(SshError::from(format!( - "{:?} file processing exception", - path - ))); + let err_msg = format!("file processing error, error info: {}", e); + error!(err_msg); + return Err(SshError::ScpError(err_msg)); } }; self.send_end()?; @@ -444,18 +430,18 @@ impl ScpBroker { #[cfg(any(target_os = "linux", target_os = "macos"))] self.sync_permissions(scp_file, file); - log::debug!("file: [{}] download completed.", scp_file.name); + debug!("file: [{}] download completed.", scp_file.name); Ok(()) } #[cfg(windows)] - fn sync_permissions(&self, scp_file: &mut ScpFile) { + fn sync_permissions(&self, scp_file: &ScpFile) { let modify_time = filetime::FileTime::from_unix_time(scp_file.modify_time, 0); let access_time = filetime::FileTime::from_unix_time(scp_file.access_time, 0); if let Err(e) = filetime::set_file_times(scp_file.local_path.as_path(), access_time, modify_time) { - log::error!( + error!( "the file time synchronization is abnormal,\ which may be caused by the operating system,\ which does not affect subsequent operations.\ @@ -466,13 +452,13 @@ impl ScpBroker { } #[cfg(any(target_os = "linux", target_os = "macos"))] - fn sync_permissions(&self, scp_file: &mut ScpFile, file: fs::File) { + fn sync_permissions(&self, scp_file: &ScpFile, file: fs::File) { let modify_time = filetime::FileTime::from_unix_time(scp_file.modify_time, 0); let access_time = filetime::FileTime::from_unix_time(scp_file.access_time, 0); if let Err(e) = filetime::set_file_times(scp_file.local_path.as_path(), access_time, modify_time) { - log::error!( + error!( "the file time synchronization is abnormal,\ which may be caused by the operating system,\ which does not affect subsequent operations.\ @@ -495,14 +481,14 @@ impl ScpBroker { .set_permissions(fs::Permissions::from_mode(mode)) .is_err() { - log::error!( + error!( "the operating system does not allow modification of file permissions, \ which does not affect subsequent operations." ); } } Err(v) => { - log::error!("Unknown error {}", v) + error!("Unknown error {}", v) } } } diff --git a/src/channel/backend/channel_shell.rs b/src/channel/backend/channel_shell.rs index bd075d8..f3e368e 100644 --- a/src/channel/backend/channel_shell.rs +++ b/src/channel/backend/channel_shell.rs @@ -1,5 +1,5 @@ use super::channel::ChannelBroker; -use crate::constant::{ssh_msg_code, ssh_str}; +use crate::constant::{ssh_connection_code, ssh_str}; use crate::error::SshResult; use crate::model::Data; use crate::TerminalSize; @@ -9,7 +9,7 @@ pub struct ShellBrocker(ChannelBroker); impl ShellBrocker { pub(crate) fn open(channel: ChannelBroker, tv: TerminalSize) -> SshResult { - // shell 形式需要一个伪终端 + // to open a shell channel, we need to request a pesudo-terminal let mut channel_shell = ShellBrocker(channel); channel_shell.request_pty(tv)?; channel_shell.get_shell()?; @@ -19,7 +19,7 @@ impl ShellBrocker { fn request_pty(&mut self, tv: TerminalSize) -> SshResult<()> { let tvs = tv.fetch(); let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_REQUEST) + data.put_u8(ssh_connection_code::CHANNEL_REQUEST) .put_u32(self.server_channel_no) .put_str(ssh_str::PTY_REQ) .put_u8(true as u8) @@ -41,7 +41,7 @@ impl ShellBrocker { fn get_shell(&mut self) -> SshResult<()> { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_REQUEST) + data.put_u8(ssh_connection_code::CHANNEL_REQUEST) .put_u32(self.server_channel_no) .put_str(ssh_str::SHELL) .put_u8(true as u8); diff --git a/src/channel/backend/mod.rs b/src/channel/backend/mod.rs index c2dc827..a95cf02 100644 --- a/src/channel/backend/mod.rs +++ b/src/channel/backend/mod.rs @@ -1,10 +1,13 @@ mod channel; mod channel_exec; -mod channel_scp; mod channel_shell; pub(crate) use channel::Channel; pub use channel::ChannelBroker; pub use channel_exec::ExecBroker; -pub use channel_scp::ScpBroker; pub use channel_shell::ShellBrocker; + +#[cfg(feature = "scp")] +mod channel_scp; +#[cfg(feature = "scp")] +pub use channel_scp::ScpBroker; diff --git a/src/channel/local/channel.rs b/src/channel/local/channel.rs index ed8c64c..c3648bd 100644 --- a/src/channel/local/channel.rs +++ b/src/channel/local/channel.rs @@ -1,17 +1,19 @@ use std::io::{Read, Write}; -use crate::channel::local::ChannelSftp; -use crate::model::TerminalSize; use crate::{ algorithm::Digest, client::Client, config::algorithm::AlgList, - constant::ssh_msg_code, + constant::ssh_connection_code, error::{SshError, SshResult}, model::{Data, FlowControl, Packet, RcMut, SecPacket}, }; +use crate::{constant::ssh_transport_code, model::TerminalSize}; +use tracing::*; -use super::{ChannelExec, ChannelScp, ChannelShell}; +#[cfg(feature = "scp")] +use super::ChannelScp; +use super::{ChannelExec, ChannelShell}; pub(super) enum ChannelRead { Data(Vec), @@ -56,14 +58,15 @@ where /// convert the raw channel to an [self::ChannelExec] /// pub fn exec(self) -> SshResult> { - log::info!("exec opened."); + info!("exec opened."); Ok(ChannelExec::open(self)) } /// convert the raw channel to an [self::ChannelScp] /// + #[cfg(feature = "scp")] pub fn scp(self) -> SshResult> { - log::info!("scp opened."); + info!("scp opened."); Ok(ChannelScp::open(self)) } @@ -72,19 +75,14 @@ where /// with `row` lines & `column` characters per one line /// pub fn shell(self, tv: TerminalSize) -> SshResult> { - log::info!("shell opened."); + info!("shell opened."); ChannelShell::open(self, tv) } - pub fn sftp(self) -> SshResult> { - log::info!("sftp opened."); - ChannelSftp::open(self) - } - /// close the channel gracefully, but donnot consume it /// pub fn close(&mut self) -> SshResult<()> { - log::info!("channel close."); + info!("channel close."); self.send_close()?; self.receive_close() } @@ -94,7 +92,7 @@ where return Ok(()); } let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_CLOSE) + data.put_u8(ssh_connection_code::CHANNEL_CLOSE) .put_u32(self.server_channel_no); self.local_close = true; self.send(data) @@ -124,7 +122,7 @@ where // send it let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_DATA) + data.put_u8(ssh_connection_code::CHANNEL_DATA) .put_u32(self.server_channel_no) .put_u8s(&buf); self.send(data)?; @@ -149,10 +147,6 @@ where Ok(maybe_response) } - pub(crate) fn send_sftp_data(&mut self, ) { - - } - /// this method will receive at least one data packet /// pub(super) fn recv(&mut self) -> SshResult> { @@ -203,7 +197,7 @@ where fn handle_msg(&mut self, mut data: Data) -> SshResult { let message_code = data.get_u8(); match message_code { - x @ ssh_msg_code::SSH_MSG_KEXINIT => { + x @ ssh_transport_code::KEXINIT => { data.insert(0, message_code); let mut digest = Digest::new(); digest.hash_ctx.set_i_s(&data); @@ -215,7 +209,7 @@ where )?; Ok(ChannelRead::Code(x)) } - x @ ssh_msg_code::SSH_MSG_CHANNEL_DATA => { + x @ ssh_connection_code::CHANNEL_DATA => { let cc = data.get_u32(); if cc == self.client_channel_no { let mut data = data.get_u8s(); @@ -228,33 +222,51 @@ where } Ok(ChannelRead::Code(x)) } - x @ ssh_msg_code::SSH_MSG_GLOBAL_REQUEST => { + x @ ssh_connection_code::CHANNEL_EXTENDED_DATA => { + let cc = data.get_u32(); + if cc == self.client_channel_no { + let data_type_code = data.get_u32(); + let mut data = data.get_u8s(); + + debug!("Recv extended data with type {data_type_code}"); + + // flow_contrl + self.flow_control.tune_on_recv(&mut data); + self.send_window_adjust(data.len() as u32)?; + + return Ok(ChannelRead::Data(data)); + } + Ok(ChannelRead::Code(x)) + } + x @ ssh_connection_code::GLOBAL_REQUEST => { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_REQUEST_FAILURE); + data.put_u8(ssh_connection_code::REQUEST_FAILURE); self.send(data)?; Ok(ChannelRead::Code(x)) } - x @ ssh_msg_code::SSH_MSG_CHANNEL_WINDOW_ADJUST => { + x @ ssh_connection_code::CHANNEL_WINDOW_ADJUST => { data.get_u32(); // to add let rws = data.get_u32(); self.recv_window_adjust(rws)?; Ok(ChannelRead::Code(x)) } - x @ ssh_msg_code::SSH_MSG_CHANNEL_EOF => { - log::debug!("Currently ignore message {}", x); + x @ ssh_connection_code::CHANNEL_EOF => { + debug!("Currently ignore message {}", x); Ok(ChannelRead::Code(x)) } - x @ ssh_msg_code::SSH_MSG_CHANNEL_REQUEST => { - log::debug!("Currently ignore message {}", x); + x @ ssh_connection_code::CHANNEL_REQUEST => { + debug!("Currently ignore message {}", x); Ok(ChannelRead::Code(x)) } - x @ ssh_msg_code::SSH_MSG_CHANNEL_SUCCESS => { - log::debug!("Currently ignore message {}", x); + x @ ssh_connection_code::CHANNEL_SUCCESS => { + debug!("Currently ignore message {}", x); Ok(ChannelRead::Code(x)) } - ssh_msg_code::SSH_MSG_CHANNEL_FAILURE => Err(SshError::from("channel failure.")), - x @ ssh_msg_code::SSH_MSG_CHANNEL_CLOSE => { + ssh_connection_code::CHANNEL_FAILURE => { + Err(SshError::GeneralError("channel failure.".to_owned())) + } + x @ ssh_connection_code::CHANNEL_CLOSE => { let cc = data.get_u32(); if cc == self.client_channel_no { self.remote_close = true; @@ -263,7 +275,7 @@ where Ok(ChannelRead::Code(x)) } x => { - log::debug!("Currently ignore message {}", x); + debug!("Currently ignore message {}", x); Ok(ChannelRead::Code(x)) } } @@ -271,7 +283,7 @@ where fn send_window_adjust(&mut self, to_add: u32) -> SshResult<()> { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_WINDOW_ADJUST) + data.put_u8(ssh_connection_code::CHANNEL_WINDOW_ADJUST) .put_u32(self.server_channel_no) .put_u32(to_add); self.flow_control.on_send(to_add); diff --git a/src/channel/local/channel_exec.rs b/src/channel/local/channel_exec.rs index ac0458e..c115388 100644 --- a/src/channel/local/channel_exec.rs +++ b/src/channel/local/channel_exec.rs @@ -1,5 +1,5 @@ use super::channel::Channel; -use crate::constant::{ssh_msg_code, ssh_str}; +use crate::constant::{ssh_connection_code, ssh_str}; use crate::error::SshResult; use crate::model::Data; use std::{ @@ -18,9 +18,9 @@ where } fn exec_command(&mut self, command: &str) -> SshResult<()> { - log::debug!("Send command {}", command); + tracing::debug!("Send command {}", command); let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_REQUEST) + data.put_u8(ssh_connection_code::CHANNEL_REQUEST) .put_u32(self.server_channel_no) .put_str(ssh_str::EXEC) .put_u8(true as u8) diff --git a/src/channel/local/channel_scp.rs b/src/channel/local/channel_scp.rs index c6b0868..e12283a 100644 --- a/src/channel/local/channel_scp.rs +++ b/src/channel/local/channel_scp.rs @@ -6,7 +6,7 @@ use crate::{ util::{check_path, file_time}, }; use crate::{ - constant::{ssh_msg_code, ssh_str}, + constant::{ssh_connection_code, ssh_str}, error::SshError, }; use crate::{model::Data, util}; @@ -16,8 +16,10 @@ use std::{ io::{Read, Write}, ops::{Deref, DerefMut}, path::Path, + str::FromStr, time::SystemTime, }; +use tracing::*; pub struct ChannelScp(Channel); @@ -31,7 +33,7 @@ where fn exec_scp(&mut self, command: &str) -> SshResult<()> { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_REQUEST) + data.put_u8(ssh_connection_code::CHANNEL_REQUEST) .put_u32(self.server_channel_no) .put_str(ssh_str::EXEC) .put_u8(true as u8) @@ -60,8 +62,8 @@ where match vec[0] { scp::END => Ok(()), // error - scp::ERR | scp::FATAL_ERR => Err(SshError::from(util::from_utf8(vec)?)), - _ => Err(SshError::from("unknown error.")), + scp::ERR | scp::FATAL_ERR => Err(SshError::ScpError(String::from_utf8(vec)?)), + _ => Err(SshError::ScpError("unknown error.".to_owned())), } } @@ -97,11 +99,10 @@ where let remote_path_str = remote_path.to_str().unwrap(); let local_path_str = local_path.to_str().unwrap(); - log::info!( + info!( "start to upload files, \ local [{}] files will be synchronized to the remote [{}] folder.", - local_path_str, - remote_path_str + local_path_str, remote_path_str ); self.exec_scp(self.command_init(remote_path_str, scp::SINK).as_str())?; @@ -110,27 +111,25 @@ where scp_file.local_path = local_path.to_path_buf(); self.file_all(&mut scp_file)?; - log::info!("files upload successful."); + info!("files upload successful."); self.close() } fn file_all(&mut self, scp_file: &mut ScpFile) -> SshResult<()> { - // 如果获取不到文件或者目录名的话,就不处理该数据 - // 如果文件不是有效的Unicode数据的话,也不处理 + // test if input file path valid scp_file.name = match scp_file.local_path.file_name() { None => return Ok(()), Some(name) => match name.to_str() { None => return Ok(()), - Some(name) => name.to_string(), + Some(name) => name.to_owned(), }, }; self.send_time(scp_file)?; if scp_file.local_path.is_dir() { - // 文件夹如果读取异常的话。就略过该文件夹 - // 详细的错误信息请查看 [std::fs::read_dir] 方法介绍 + // skip the read_dir errs if let Err(e) = fs::read_dir(scp_file.local_path.as_path()) { - log::error!("read dir error, error info: {}", e); + error!("read dir error, error info: {}", e); return Ok(()); } self.send_dir(scp_file)?; @@ -141,13 +140,13 @@ where self.file_all(scp_file)? } Err(e) => { - // 暂不处理 - log::error!("dir entry error, error info: {}", e); + // TODO + error!("dir entry error, error info: {}", e); } } } - self.send_bytes(&[scp::E as u8, b'\n'])?; + self.send_bytes(&[scp::E, b'\n'])?; self.get_end()?; } else { scp_file.size = scp_file.local_path.as_path().metadata()?.len(); @@ -156,12 +155,11 @@ where Ok(()) } - fn send_file(&mut self, scp_file: &mut ScpFile) -> SshResult<()> { + fn send_file(&mut self, scp_file: &ScpFile) -> SshResult<()> { let mut file = match File::open(scp_file.local_path.as_path()) { Ok(f) => f, - // 文件打开异常,不影响后续操作 Err(e) => { - log::error!( + error!( "failed to open the folder, \ it is possible that the path does not exist, \ which does not affect subsequent operations. \ @@ -172,10 +170,9 @@ where } }; - log::debug!( + debug!( "name: [{}] size: [{}] type: [file] start upload.", - scp_file.name, - scp_file.size + scp_file.name, scp_file.size ); let cmd = format!( @@ -200,13 +197,13 @@ where } self.get_end()?; - log::debug!("file: [{}] upload completed.", scp_file.name); + debug!("file: [{}] upload completed.", scp_file.name); Ok(()) } fn send_dir(&mut self, scp_file: &ScpFile) -> SshResult<()> { - log::debug!( + debug!( "name: [{}] size: [0], type: [dir] start upload.", scp_file.name ); @@ -215,7 +212,7 @@ where self.send_bytes(cmd.as_bytes())?; self.get_end()?; - log::debug!("dir: [{}] upload completed.", scp_file.name); + debug!("dir: [{}] upload completed.", scp_file.name); Ok(()) } @@ -229,14 +226,12 @@ where fn get_time(&self, scp_file: &mut ScpFile) -> SshResult<()> { let metadata = scp_file.local_path.as_path().metadata()?; - // 最后修改时间 let modified_time = match metadata.modified() { Ok(t) => t, Err(_) => SystemTime::now(), }; let modified_time = util::sys_time_to_secs(modified_time)?; - // 最后访问时间 let accessed_time = match metadata.accessed() { Ok(t) => t, Err(_) => SystemTime::now(), @@ -275,11 +270,10 @@ where let local_path_str = local_path.to_str().unwrap(); let remote_path_str = remote_path.to_str().unwrap(); - log::info!( + info!( "start to download files, \ remote [{}] files will be synchronized to the local [{}] folder.", - remote_path_str, - local_path_str + remote_path_str, local_path_str ); self.exec_scp(self.command_init(remote_path_str, scp::SOURCE).as_str())?; @@ -287,7 +281,7 @@ where scp_file.local_path = local_path.to_path_buf(); self.process_d(&mut scp_file, local_path)?; - log::info!("files download successful."); + info!("files download successful."); self.close() } @@ -302,7 +296,6 @@ where let code = &data[0]; match *code { scp::T => { - // 处理时间 let (modify_time, access_time) = file_time(data)?; scp_file.modify_time = modify_time; scp_file.access_time = access_time; @@ -319,15 +312,17 @@ where } }, // error - scp::ERR | scp::FATAL_ERR => return Err(SshError::from(util::from_utf8(data)?)), - _ => return Err(SshError::from("unknown error.")), + scp::ERR | scp::FATAL_ERR => { + return Err(SshError::ScpError(String::from_utf8(data)?)) + } + _ => return Err(SshError::ScpError("unknown error.".to_owned())), } } Ok(()) } fn process_dir_d(&mut self, data: Vec, scp_file: &mut ScpFile) -> SshResult<()> { - let string = util::from_utf8(data)?; + let string = String::from_utf8(data)?; let dir_info = string.trim(); let split = dir_info.split(' ').collect::>(); match split.get(2) { @@ -336,7 +331,7 @@ where } scp_file.is_dir = true; let buf = scp_file.join(&scp_file.name); - log::debug!( + debug!( "name: [{}] size: [0], type: [dir] start download.", scp_file.name ); @@ -356,29 +351,23 @@ where self.sync_permissions(scp_file, file); } Err(e) => { - log::error!( - "failed to open the folder, \ - it is possible that the path does not exist, \ - which does not affect subsequent operations. \ - error info: {:?}, path: {:?}", - e, - scp_file.local_path.to_str() - ); - return Err(SshError::from(format!("file open error: {}", e))); + let err_msg = format!("file open error: {}", e); + error!(err_msg); + return Err(SshError::ScpError(err_msg)); } }; } - log::debug!("dir: [{}] download completed.", scp_file.name); + debug!("dir: [{}] download completed.", scp_file.name); Ok(()) } fn process_file_d(&mut self, data: Vec, scp_file: &mut ScpFile) -> SshResult<()> { - let string = util::from_utf8(data)?; + let string = String::from_utf8(data)?; let file_info = string.trim(); let split = file_info.split(' ').collect::>(); let size_str = *split.get(1).unwrap_or(&"0"); - let size = util::str_to_i64(size_str)?; + let size = i64::from_str(size_str)?; scp_file.size = size as u64; match split.get(2) { None => return Ok(()), @@ -389,10 +378,9 @@ where } fn save_file(&mut self, scp_file: &mut ScpFile) -> SshResult<()> { - log::debug!( + debug!( "name: [{}] size: [{}] type: [file] start download.", - scp_file.name, - scp_file.size + scp_file.name, scp_file.size ); let path = scp_file.join(&scp_file.name); if path.exists() { @@ -406,11 +394,9 @@ where { Ok(v) => v, Err(e) => { - log::error!("file processing error, error info: {}", e); - return Err(SshError::from(format!( - "{:?} file processing exception", - path - ))); + let err_msg = format!("file open error: {}", e); + error!(err_msg); + return Err(SshError::ScpError(err_msg)); } }; self.send_end()?; @@ -438,7 +424,7 @@ where #[cfg(any(target_os = "linux", target_os = "macos"))] self.sync_permissions(scp_file, file); - log::debug!("file: [{}] download completed.", scp_file.name); + debug!("file: [{}] download completed.", scp_file.name); Ok(()) } @@ -449,7 +435,7 @@ where if let Err(e) = filetime::set_file_times(scp_file.local_path.as_path(), access_time, modify_time) { - log::error!( + error!( "the file time synchronization is abnormal,\ which may be caused by the operating system,\ which does not affect subsequent operations.\ @@ -460,13 +446,13 @@ where } #[cfg(any(target_os = "linux", target_os = "macos"))] - fn sync_permissions(&self, scp_file: &mut ScpFile, file: fs::File) { + fn sync_permissions(&self, scp_file: &ScpFile, file: fs::File) { let modify_time = filetime::FileTime::from_unix_time(scp_file.modify_time, 0); let access_time = filetime::FileTime::from_unix_time(scp_file.access_time, 0); if let Err(e) = filetime::set_file_times(scp_file.local_path.as_path(), access_time, modify_time) { - log::error!( + error!( "the file time synchronization is abnormal,\ which may be caused by the operating system,\ which does not affect subsequent operations.\ @@ -489,14 +475,14 @@ where .set_permissions(fs::Permissions::from_mode(mode)) .is_err() { - log::error!( + error!( "the operating system does not allow modification of file permissions, \ which does not affect subsequent operations." ); } } Err(v) => { - log::error!("Unknown error {}", v) + error!("Unknown error {}", v) } } } diff --git a/src/channel/local/channel_shell.rs b/src/channel/local/channel_shell.rs index f1e6c0f..1000f80 100644 --- a/src/channel/local/channel_shell.rs +++ b/src/channel/local/channel_shell.rs @@ -1,5 +1,5 @@ use super::channel::Channel; -use crate::constant::{ssh_msg_code, ssh_str}; +use crate::constant::{ssh_connection_code, ssh_str}; use crate::error::SshResult; use crate::model::{Data, TerminalSize}; use std::{ @@ -14,7 +14,7 @@ where S: Read + Write, { pub(crate) fn open(channel: Channel, tv: TerminalSize) -> SshResult { - // shell 形式需要一个伪终端 + // to open a shell channel, we need to request a pesudo-terminal let mut channel_shell = ChannelShell(channel); channel_shell.request_pty(tv)?; channel_shell.get_shell()?; @@ -23,9 +23,8 @@ where fn request_pty(&mut self, tv: TerminalSize) -> SshResult<()> { let tvs = tv.fetch(); - println!("tvs {:?}", tvs); let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_REQUEST) + data.put_u8(ssh_connection_code::CHANNEL_REQUEST) .put_u32(self.server_channel_no) .put_str(ssh_str::PTY_REQ) .put_u8(false as u8) @@ -47,7 +46,7 @@ where fn get_shell(&mut self) -> SshResult<()> { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_REQUEST) + data.put_u8(ssh_connection_code::CHANNEL_REQUEST) .put_u32(self.server_channel_no) .put_str(ssh_str::SHELL) .put_u8(false as u8); diff --git a/src/channel/local/mod.rs b/src/channel/local/mod.rs index 5f68236..54cd355 100644 --- a/src/channel/local/mod.rs +++ b/src/channel/local/mod.rs @@ -1,11 +1,12 @@ mod channel; mod channel_exec; -mod channel_scp; -mod channel_sftp; mod channel_shell; pub(crate) use channel::Channel; pub use channel_exec::ChannelExec; -pub use channel_scp::ChannelScp; -pub use channel_sftp::ChannelSftp; pub use channel_shell::ChannelShell; + +#[cfg(feature = "scp")] +mod channel_scp; +#[cfg(feature = "scp")] +pub use channel_scp::ChannelScp; diff --git a/src/channel/mod.rs b/src/channel/mod.rs index 05ce35c..81fe799 100644 --- a/src/channel/mod.rs +++ b/src/channel/mod.rs @@ -2,10 +2,13 @@ mod backend; mod local; pub(crate) use backend::Channel as BackendChannel; -pub use backend::{ChannelBroker, ExecBroker, ScpBroker, ShellBrocker}; +pub use backend::{ChannelBroker, ExecBroker, ShellBrocker}; pub(crate) use local::Channel as LocalChannel; pub use local::ChannelExec as LocalExec; -pub use local::ChannelScp as LocalScp; -pub use local::ChannelSftp as LocalSftp; pub use local::ChannelShell as LocalShell; + +#[cfg(feature = "scp")] +pub use backend::ScpBroker; +#[cfg(feature = "scp")] +pub use local::ChannelScp as LocalScp; diff --git a/src/client/client.rs b/src/client/client.rs index 17dde16..202e4e6 100644 --- a/src/client/client.rs +++ b/src/client/client.rs @@ -1,7 +1,10 @@ +use crate::{ + algorithm::compression::{CompressNone, Compression}, + config::algorithm::AlgList, +}; use crate::{algorithm::encryption::Encryption, config::Config}; - -use crate::config::algorithm::AlgList; use crate::{algorithm::encryption::EncryptionNone, model::Sequence}; +use std::time::Duration; // the underlay connection pub(crate) struct Client { @@ -9,6 +12,7 @@ pub(crate) struct Client { pub(super) config: Config, pub(super) negotiated: AlgList, pub(super) encryptor: Box, + pub(super) compressor: Box, pub(super) session_id: Vec, } @@ -16,7 +20,8 @@ impl Client { pub fn new(config: Config) -> Self { Self { config, - encryptor: Box::new(EncryptionNone::default()), + encryptor: Box::::default(), + compressor: Box::::default(), negotiated: AlgList::new(), session_id: vec![], sequence: Sequence::new(), @@ -27,15 +32,19 @@ impl Client { self.encryptor.as_mut() } + pub fn get_compressor(&mut self) -> &mut dyn Compression { + self.compressor.as_mut() + } + pub fn get_seq(&mut self) -> &mut Sequence { &mut self.sequence } - pub fn get_timeout(&self) -> u128 { + pub fn get_timeout(&self) -> Option { self.config.timeout } - pub fn set_timeout(&mut self, tm: u128) { + pub fn set_timeout(&mut self, tm: Option) { self.config.timeout = tm } } diff --git a/src/client/client_auth.rs b/src/client/client_auth.rs index 52cbd33..aafde4e 100644 --- a/src/client/client_auth.rs +++ b/src/client/client_auth.rs @@ -1,8 +1,9 @@ use std::io::{Read, Write}; +use tracing::*; use crate::{ - algorithm::Digest, - constant::{ssh_msg_code, ssh_str}, + algorithm::{compression, Compress, Digest}, + constant::{ssh_connection_code, ssh_str, ssh_transport_code, ssh_user_auth_code}, error::{SshError, SshResult}, model::{Data, Packet, SecPacket}, }; @@ -10,13 +11,13 @@ use crate::{ use super::Client; impl Client { - pub fn do_auth(&mut self, stream: &mut S, digest: &mut Digest) -> SshResult<()> + pub fn do_auth(&mut self, stream: &mut S, digest: &Digest) -> SshResult<()> where S: Read + Write, { - log::info!("Auth start"); + info!("Auth start"); let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_SERVICE_REQUEST) + data.put_u8(ssh_transport_code::SERVICE_REQUEST) .put_str(ssh_str::SSH_USERAUTH); data.pack(self).write_stream(stream)?; @@ -25,7 +26,7 @@ impl Client { let mut data = Data::unpack(SecPacket::from_stream(stream, self)?)?; let message_code = data.get_u8(); match message_code { - ssh_msg_code::SSH_MSG_SERVICE_ACCEPT => { + ssh_transport_code::SERVICE_ACCEPT => { if self.config.auth.key_pair.is_none() { tried_public_key = true; // if no private key specified @@ -37,31 +38,37 @@ impl Client { self.public_key_authentication(stream)? } } - ssh_msg_code::SSH_MSG_USERAUTH_FAILURE => { + ssh_user_auth_code::FAILURE => { if !tried_public_key { - log::error!("user auth failure. (public key)"); - log::info!("fallback to password authentication"); + error!("user auth failure. (public key)"); + info!("fallback to password authentication"); tried_public_key = true; // keep the same with openssh // if the public key auth failed // try with password again self.password_authentication(stream)? } else { - log::error!("user auth failure. (password)"); - return Err(SshError::from("user auth failure.")); + error!("user auth failure. (password)"); + return Err(SshError::AuthError); } } - ssh_msg_code::SSH_MSG_USERAUTH_PK_OK => { - log::info!("user auth support this algorithm."); + ssh_user_auth_code::PK_OK => { + info!("user auth support this algorithm."); self.public_key_signature(stream, digest)? } - ssh_msg_code::SSH_MSG_USERAUTH_SUCCESS => { - log::info!("user auth successful."); + ssh_user_auth_code::SUCCESS => { + info!("user auth successful."); + // + // Now we need turn on the compressor if any + if let Compress::ZlibOpenSsh = self.negotiated.c_compress[0] { + let comp = compression::from(&Compress::ZlibOpenSsh); + self.compressor = comp; + } return Ok(()); } - ssh_msg_code::SSH_MSG_GLOBAL_REQUEST => { + ssh_connection_code::GLOBAL_REQUEST => { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_REQUEST_FAILURE); + data.put_u8(ssh_connection_code::REQUEST_FAILURE); data.pack(self).write_stream(stream)?; } _ => {} @@ -73,9 +80,9 @@ impl Client { where S: Write, { - log::info!("password authentication."); + info!("password authentication."); let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_USERAUTH_REQUEST) + data.put_u8(ssh_user_auth_code::REQUEST) .put_str(self.config.auth.username.as_str()) .put_str(ssh_str::SSH_CONNECTION) .put_str(ssh_str::PASSWORD) @@ -91,12 +98,12 @@ impl Client { { let data = { let pubkey_alg = &self.negotiated.public_key[0]; - log::info!( + info!( "public key authentication. algorithm: {}", pubkey_alg.as_ref() ); let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_USERAUTH_REQUEST) + data.put_u8(ssh_user_auth_code::REQUEST) .put_str(self.config.auth.username.as_str()) .put_str(ssh_str::SSH_CONNECTION) .put_str(ssh_str::PUBLIC_KEY) @@ -128,7 +135,7 @@ impl Client { let pubkey_alg = &self.negotiated.public_key[0]; let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_USERAUTH_REQUEST) + data.put_u8(ssh_user_auth_code::REQUEST) .put_str(self.config.auth.username.as_str()) .put_str(ssh_str::SSH_CONNECTION) .put_str(ssh_str::PUBLIC_KEY) diff --git a/src/client/client_kex.rs b/src/client/client_kex.rs index bac9de3..dbe37e4 100644 --- a/src/client/client_kex.rs +++ b/src/client/client_kex.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "deprecated-zlib")] +use crate::algorithm::{compression, Compress}; use crate::{ algorithm::{ encryption, @@ -8,12 +10,13 @@ use crate::{ Digest, }, client::Client, - config::{algorithm::AlgList, version::SshVersion}, - constant::ssh_msg_code, + config::algorithm::AlgList, + constant::ssh_transport_code, error::{SshError, SshResult}, model::{Data, Packet, SecPacket}, }; use std::io::{Read, Write}; +use tracing::*; impl Client { pub fn key_agreement( @@ -26,13 +29,11 @@ impl Client { S: Read + Write, { // initialize the hash context - if let SshVersion::V2(ref our, ref their) = self.config.ver { - digest.hash_ctx.set_v_c(our); - digest.hash_ctx.set_v_s(their); - } + digest.hash_ctx.set_v_c(&self.config.ver.client_ver); + digest.hash_ctx.set_v_s(&self.config.ver.server_ver); - log::info!("start for key negotiation."); - log::info!("send client algorithm list."); + info!("start for key negotiation."); + info!("send client algorithm list."); let algs = self.config.algs.clone(); let client_algs = algs.pack(self); @@ -79,9 +80,18 @@ impl Client { self.session_id = session_id; self.negotiated = negotiated; self.encryptor = encryption; + + #[cfg(feature = "deprecated-zlib")] + { + if let Compress::Zlib = self.negotiated.c_compress[0] { + let comp = compression::from(&Compress::Zlib); + self.compressor = comp; + } + } + digest.key_exchange = Some(key_exchange); - log::info!("key negotiation successful."); + info!("key negotiation successful."); Ok(()) } @@ -92,7 +102,7 @@ impl Client { S: Read + Write, { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_KEXDH_INIT) + data.put_u8(ssh_transport_code::KEXDH_INIT) .put_u8s(public_key); data.pack(self).write_stream(stream) } @@ -112,19 +122,20 @@ impl Client { let mut data = Data::unpack(SecPacket::from_stream(stream, self)?)?; let message_code = data.get_u8(); match message_code { - ssh_msg_code::SSH_MSG_KEXDH_REPLY => { - // 生成session_id并且获取signature + ssh_transport_code::KEXDH_REPLY => { + // Generate the session id, get the signature let sig = self.generate_signature(data, h, key_exchange)?; - // 验签 + // verify the signature session_id = hash::digest(&h.as_bytes(), key_exchange.get_hash_type()); let flag = public_key.verify_signature(&h.k_s, &session_id, &sig)?; if !flag { - log::error!("signature verification failure."); - return Err(SshError::from("signature verification failure.")); + let err_msg = "signature verification failure.".to_owned(); + error!(err_msg); + return Err(SshError::KexError(err_msg)); } - log::info!("signature verification success."); + info!("signature verification success."); } - ssh_msg_code::SSH_MSG_NEWKEYS => { + ssh_transport_code::NEWKEYS => { self.new_keys(stream)?; return Ok(session_id); } @@ -133,7 +144,7 @@ impl Client { } } - /// 生成签名 + /// get the signature fn generate_signature( &mut self, mut data: Data, @@ -142,10 +153,11 @@ impl Client { ) -> SshResult> { let ks = data.get_u8s(); h.set_k_s(&ks); - // TODO 未进行密钥指纹验证!! + // TODO: + // No fingerprint verification let qs = data.get_u8s(); - h.set_q_c(key_exchange.get_public_key()); - h.set_q_s(&qs); + h.set_e(key_exchange.get_public_key()); + h.set_f(&qs); let vec = key_exchange.get_shared_secret(qs)?; h.set_k(&vec); let h = data.get_u8s(); @@ -155,14 +167,14 @@ impl Client { Ok(signature) } - /// SSH_MSG_NEWKEYS 代表密钥交换完成 + /// NEWKEYS indicates that kex is done fn new_keys(&mut self, stream: &mut S) -> SshResult<()> where S: Write, { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_NEWKEYS); - log::info!("send new keys"); + data.put_u8(ssh_transport_code::NEWKEYS); + info!("send new keys"); data.pack(self).write_stream(stream) } } diff --git a/src/config/algorithm.rs b/src/config/algorithm.rs index 6305f0e..7ce9027 100644 --- a/src/config/algorithm.rs +++ b/src/config/algorithm.rs @@ -3,11 +3,12 @@ use std::{ ops::{Deref, DerefMut}, str::FromStr, }; +use tracing::*; use crate::{ algorithm::{Compress, Enc, Kex, Mac, PubKey}, client::Client, - constant::ssh_msg_code, + constant::ssh_transport_code, error::{SshError, SshResult}, model::{Data, Packet, SecPacket}, util, @@ -126,21 +127,21 @@ impl AlgList { .into(), c_mac: vec![Mac::HmacSha2_256, Mac::HmacSha2_512, Mac::HmacSha1].into(), s_mac: vec![Mac::HmacSha2_256, Mac::HmacSha2_512, Mac::HmacSha1].into(), - c_compress: vec![Compress::None].into(), - s_compress: vec![Compress::None].into(), + c_compress: vec![Compress::None, Compress::ZlibOpenSsh].into(), + s_compress: vec![Compress::None, Compress::ZlibOpenSsh].into(), } } fn from(mut data: Data) -> SshResult { data.get_u8(); - // 跳过16位cookie + // skip the 16-bit cookie data.skip(16); let mut server_algorithm = Self::new(); macro_rules! try_convert { ($hint: literal, $field: ident) => { let alg_string = util::vec_u8_to_string(data.get_u8s(), ",")?; - log::info!("server {}: {:?}", $hint, alg_string); + info!("server {}: {:?}", $hint, alg_string); server_algorithm.$field = alg_string.try_into()?; }; } @@ -152,7 +153,7 @@ impl AlgList { try_convert!("s2c mac", s_mac); try_convert!("c2s compression", c_compress); try_convert!("s2c compression", s_compress); - log::debug!("converted server algorithms: [{:?}]", server_algorithm); + debug!("converted server algorithms: [{:?}]", server_algorithm); Ok(server_algorithm) } @@ -169,15 +170,14 @@ impl AlgList { } }) .ok_or_else(|| { - log::error!( + let err_msg = format!( "Key_agreement: the {} fails to match, \ - algorithms supported by the server: {},\ - algorithms supported by the client: {}", - $err_hint, - $their.$field, - $our.$field + algorithms supported by the server: {},\ + algorithms supported by the client: {}", + $err_hint, $their.$field, $our.$field ); - SshError::from("key exchange error.") + error!(err_msg); + SshError::KexError(err_msg) }) }; } @@ -209,7 +209,7 @@ impl AlgList { s_compress: vec![*s_compress].into(), }; - log::info!("matched algorithms [{:?}]", negotiated); + info!("matched algorithms [{:?}]", negotiated); Ok(negotiated) } @@ -230,9 +230,9 @@ impl AlgList { impl<'a> Packet<'a> for AlgList { fn pack(self, client: &'a mut Client) -> crate::model::SecPacket<'a> { - log::info!("client algorithms: [{:?}]", self); + info!("client algorithms: [{:?}]", self); let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_KEXINIT); + data.put_u8(ssh_transport_code::KEXINIT); data.extend(util::cookie()); data.extend(self.as_i()); data.put_str("") @@ -248,7 +248,7 @@ impl<'a> Packet<'a> for AlgList { Self: Sized, { let data = pkt.into_inner(); - assert_eq!(data[0], ssh_msg_code::SSH_MSG_KEXINIT); + assert_eq!(data[0], ssh_transport_code::KEXINIT); AlgList::from(data) } } diff --git a/src/config/auth.rs b/src/config/auth.rs index 2cdf88e..cf4f4d9 100644 --- a/src/config/auth.rs +++ b/src/config/auth.rs @@ -5,7 +5,8 @@ use crate::algorithm::{ use crate::model::Data; use crate::{SshError, SshResult}; use rsa::pkcs1::DecodeRsaPrivateKey; -use rsa::PublicKeyParts; +use rsa::pkcs1v15::Pkcs1v15Sign; +use rsa::traits::PublicKeyParts; use std::fmt::Debug; use std::fs::File; use std::io::Read; @@ -32,16 +33,18 @@ impl KeyPair { ssh_key::Algorithm::Rsa { hash: _hash } => (KeyType::SshRsa, key_str), ssh_key::Algorithm::Ed25519 => (KeyType::SshEd25519, key_str), x => { - return Err(SshError::from(format!( + return Err(SshError::SshPubKeyError(format!( "Currently don't support the key file type {}", x ))) } }, - Err(e) => return Err(SshError::from(e.to_string())), + Err(e) => return Err(SshError::SshPubKeyError(e.to_string())), } } else { - return Err(SshError::from("Unable to detect the pulic key type")); + return Err(SshError::SshPubKeyError( + "Unable to detect the pulic key type".to_owned(), + )); }; // then store it @@ -93,16 +96,16 @@ impl KeyPair { KeyType::PemRsa | KeyType::SshRsa => { let (scheme, digest) = match alg { PubKey::RsaSha2_512 => ( - rsa::PaddingScheme::new_pkcs1v15_sign::(), + Pkcs1v15Sign::new::(), ring::digest::digest(&ring::digest::SHA512, sd), ), PubKey::RsaSha2_256 => ( - rsa::PaddingScheme::new_pkcs1v15_sign::(), + Pkcs1v15Sign::new::(), ring::digest::digest(&ring::digest::SHA256, sd), ), - #[cfg(feature = "dangerous-rsa-sha1")] + #[cfg(feature = "deprecated-rsa-sha1")] PubKey::SshRsa => ( - rsa::PaddingScheme::new_pkcs1v15_sign::(), + Pkcs1v15Sign::new::(), ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, sd), ), _ => unreachable!(), @@ -153,19 +156,14 @@ impl KeyPair { } } -#[derive(Clone)] +#[derive(Clone, Default)] pub(super) enum KeyType { + #[default] PemRsa, SshRsa, SshEd25519, } -impl Default for KeyType { - fn default() -> Self { - KeyType::PemRsa - } -} - #[derive(Clone, Default)] pub(crate) struct AuthInfo { pub username: String, @@ -209,10 +207,7 @@ impl AuthInfo { where P: AsRef, { - let mut file = match File::open(p) { - Ok(file) => file, - Err(e) => return Err(SshError::from(e.to_string())), - }; + let mut file = File::open(p)?; let mut prks = String::new(); file.read_to_string(&mut prks)?; diff --git a/src/config/mod.rs b/src/config/mod.rs index ec56b31..2f5b05e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod algorithm; pub(crate) mod auth; pub(crate) mod version; use crate::algorithm::PubKey as PubKeyAlgs; +use std::time::Duration; fn insert_or_move_first(v: &mut Vec, alg: PubKeyAlgs) { if let Some(i) = v.iter().position(|each| *each == alg) { @@ -16,7 +17,7 @@ pub(crate) struct Config { pub ver: version::SshVersion, pub auth: auth::AuthInfo, pub algs: algorithm::AlgList, - pub timeout: u128, // in milliseconds + pub timeout: Option, auto_tune: bool, } @@ -26,7 +27,7 @@ impl Default for Config { algs: algorithm::AlgList::client_default(), auth: auth::AuthInfo::default(), ver: version::SshVersion::default(), - timeout: 30 * 1000, + timeout: Some(Duration::from_secs(30)), auto_tune: true, } } @@ -39,7 +40,7 @@ impl Config { algs: algorithm::AlgList::default(), auth: auth::AuthInfo::default(), ver: version::SshVersion::default(), - timeout: 30 * 1000, + timeout: Some(Duration::from_secs(30)), auto_tune: false, } } diff --git a/src/config/version.rs b/src/config/version.rs index ed5b106..516babc 100644 --- a/src/config/version.rs +++ b/src/config/version.rs @@ -1,44 +1,80 @@ use std::io::{Read, Write}; +use std::time::Duration; +use tracing::*; use crate::{ - constant::{self, CLIENT_VERSION}, + constant::{self, CLIENT_VERSION, SSH_MAGIC}, error::{SshError, SshResult}, model::Timeout, }; -type OurVer = String; -type ServerVer = String; - #[derive(Debug, Clone)] -pub(crate) enum SshVersion { - V1, - V2(OurVer, ServerVer), - Unknown, +pub(crate) struct SshVersion { + pub client_ver: String, + pub server_ver: String, } impl Default for SshVersion { fn default() -> Self { - SshVersion::Unknown + Self { + client_ver: CLIENT_VERSION.to_owned(), + server_ver: String::new(), + } } } -fn read_version(stream: &mut S, tm: u128) -> SshResult> +/// + +// When the connection has been established, both sides MUST send an +// identification string. This identification string MUST be + +// SSH-protoversion-softwareversion SP comments CR LF + +// Since the protocol being defined in this set of documents is version +// 2.0, the 'protoversion' MUST be "2.0". The 'comments' string is +// OPTIONAL. If the 'comments' string is included, a 'space' character +// (denoted above as SP, ASCII 32) MUST separate the 'softwareversion' +// and 'comments' strings. The identification MUST be terminated by a +// single Carriage Return (CR) and a single Line Feed (LF) character +// (ASCII 13 and 10, respectively). +fn read_version(stream: &mut S, tm: Option) -> SshResult> where S: Read, { - let mut buf = vec![0; 128]; - let timeout = Timeout::new(tm); + let mut ch = vec![0; 1]; + const LF: u8 = 0xa; + let crlf = vec![0xd, 0xa]; + let mut outbuf = vec![]; + let mut timeout = Timeout::new(tm); loop { - match stream.read(&mut buf) { + match stream.read(&mut ch) { Ok(i) => { - // MY TO DO: To Skip the other lines - assert_eq!(&buf[0..4], constant::SSH_MAGIC); - buf.truncate(i); - return Ok(buf); + if 0 == i { + // eof got, return + return Ok(outbuf); + } + + outbuf.extend_from_slice(&ch); + + if LF == ch[0] && outbuf.len() > 1 && outbuf.ends_with(&crlf) { + // The server MAY send other lines of data before sending the version + // string. Each line SHOULD be terminated by a Carriage Return and Line + // Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded + // in ISO-10646 UTF-8 [RFC3629] (language is not specified). Clients + // MUST be able to process such lines. Such lines MAY be silently + // ignored, or MAY be displayed to the client user. + if outbuf.len() < 4 || &outbuf[0..4] != SSH_MAGIC { + // skip other lines + // and start read for another line + outbuf.clear(); + continue; + } + return Ok(outbuf); + } } Err(e) => { if let std::io::ErrorKind::WouldBlock = e.kind() { - timeout.test()?; + timeout.till_next_tick()?; continue; } else { return Err(e.into()); @@ -49,45 +85,46 @@ where } impl SshVersion { - pub fn from(stream: &mut S, timeout: u128) -> SshResult + pub fn read_server_version( + &mut self, + stream: &mut S, + timeout: Option, + ) -> SshResult<()> where S: Read, { let buf = read_version(stream, timeout)?; - let from_utf8 = crate::util::from_utf8(buf)?; + if buf.len() < 4 || &buf[0..4] != SSH_MAGIC { + error!("SSH version magic doesn't match"); + error!("Probably not an ssh server"); + } + let from_utf8 = String::from_utf8(buf)?; let version_str = from_utf8.trim(); - log::info!("server version: [{}]", version_str); + info!("server version: [{}]", version_str); - if version_str.contains("SSH-2.0") { - Ok(SshVersion::V2( - CLIENT_VERSION.to_string(), - version_str.to_string(), - )) - } else if version_str.contains("SSH-1.0") { - Ok(SshVersion::V1) - } else { - Ok(SshVersion::Unknown) - } + self.server_ver = version_str.to_owned(); + Ok(()) } - pub fn write(stream: &mut S) -> SshResult<()> + pub fn send_our_version(&self, stream: &mut S) -> SshResult<()> where S: Write, { - log::info!("client version: [{}]", CLIENT_VERSION); - let ver_string = format!("{}\r\n", CLIENT_VERSION); + info!("client version: [{}]", self.client_ver); + let ver_string = format!("{}\r\n", self.client_ver); let _ = stream.write(ver_string.as_bytes())?; Ok(()) } pub fn validate(&self) -> SshResult<()> { - if let SshVersion::V2(_, _) = self { - log::info!("version negotiation was successful."); + if self.server_ver.contains("SSH-2.0") { Ok(()) } else { - let err_msg = "error in version negotiation, version mismatch."; - log::error!("{}", err_msg); - Err(SshError::from(err_msg)) + error!("error in version negotiation, version mismatch."); + Err(SshError::VersionDismatchError { + our: constant::CLIENT_VERSION.to_owned(), + their: self.server_ver.clone(), + }) } } } diff --git a/src/constant.rs b/src/constant.rs index 373bf7f..46da5a6 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -1,334 +1,162 @@ -/// 客户端版本 -pub(crate) const CLIENT_VERSION: &str = "SSH-2.0-SSH_RS-0.3.2"; +/// The client version +pub(crate) const CLIENT_VERSION: &str = "SSH-2.0-SSH_RS-0.4.5"; pub(crate) const SSH_MAGIC: &[u8] = b"SSH-"; -/// ssh通讯时用到的常量字符串 +/// The constant strings that used for ssh communication #[allow(dead_code)] pub(crate) mod ssh_str { - /// 准备认证 + /// Pre-auth msg pub const SSH_USERAUTH: &str = "ssh-userauth"; - /// 开始认证 + /// Authenticate msg pub const SSH_CONNECTION: &str = "ssh-connection"; - /// 公钥验证方式 + /// Authenticate with public key pub const PUBLIC_KEY: &str = "publickey"; - /// 密码认证方式 + /// Authenticate with password pub const PASSWORD: &str = "password"; - /// 打开一个会话 + /// Session level msg pub const SESSION: &str = "session"; - /// 启动一个命令解释程序 + /// Open a Shell pub const SHELL: &str = "shell"; - /// 执行一个命令 + /// Execute a command pub const EXEC: &str = "exec"; - /// 执行文件传输 + /// SCP pub const SCP: &str = "scp"; - /// 请求一个伪终端 + /// Request a pesudo-terminal pub const PTY_REQ: &str = "pty-req"; - /// 启动一个子系统 - pub const SUBSYSTEM: &str = "subsystem"; - /// sftp 子系统 - pub const SFTP: &str = "sftp"; - /// 伪终端的样式 + /// The xterm style that used for the pty pub const XTERM_VAR: &str = "xterm-256color"; } #[allow(dead_code)] pub(crate) mod permission { - /// 文件夹默认权限 + /// The default permission for directories pub const DIR: &str = "775"; - /// 文件默认权限 + /// The default permission for files pub const FILE: &str = "664"; } -/// scp 操作时用到的常量 +/// Some constants that used when scp +#[cfg(feature = "scp")] #[allow(dead_code)] pub(crate) mod scp { - // scp 参数常量 - /// 意味着当前机器上的scp,将本地文件传输到另一个scp上 + /// Scp from our to the remote pub const SOURCE: &str = "-f"; - /// 意味着当前机器上的scp,即将收到另一个scp传输过来的文件 + /// Scp from the remote to our pub const SINK: &str = "-t"; - /// 递归复制整个目录 + /// Recursive scp for a dir pub const RECURSIVE: &str = "-r"; - /// 详细方式显示输出 + /// Show details pub const VERBOSE: &str = "-v"; - /// 保留原文件的修改时间,访问时间和访问权限 + /// Keep the modification, access time and permission the same with the origin pub const PRESERVE_TIMES: &str = "-p"; - /// 不显示传输进度条 + /// Show not progress bar pub const QUIET: &str = "-q"; - /// 限定用户所能使用的带宽 + /// Limit the bandwidth usage pub const LIMIT: &str = "-l"; - // scp传输时的状态常量 - /// 代表当前接收的数据是文件的最后修改时间和最后访问时间 + /// Indicate the modification, access time of the file we recieve /// "T1647767946 0 1647767946 0\n"; pub const T: u8 = b'T'; - /// 代表当前接收的数据是文件夹 + /// Indicate that we are recieving a directory /// "D0775 0 dirName\n" pub const D: u8 = b'D'; - /// 代表当前接收的数据是文件 + /// Indicate that we are recieving a file /// "C0664 200 fileName.js\n" pub const C: u8 = b'C'; - /// 代表当前文件夹传输结束,需要返回上层文件夹 + /// Indicate that current directory is done /// "D\n" pub const E: u8 = b'E'; - /// 代表结束当前操作 + /// The end flag of current operation // '\0' pub const END: u8 = 0; - /// scp操作异常 + /// Exceptions occur pub const ERR: u8 = 1; - /// scp操作比较严重的异常 + /// Exceptions that cannot recover pub const FATAL_ERR: u8 = 2; } -/// 一些默认大小 #[allow(dead_code)] pub(crate) mod size { pub const FILE_CHUNK: usize = 30000; - /// 最大数据包大小 + /// The max size of one packet pub const BUF_SIZE: usize = 32768; - /// 默认客户端的窗口大小 + /// The default window size of the flow-control pub const LOCAL_WINDOW_SIZE: u32 = 2097152; } -/// ssh 消息码 +/// #[allow(dead_code)] -pub(crate) mod ssh_msg_code { - pub const SSH_MSG_DISCONNECT: u8 = 1; - pub const SSH_MSG_IGNORE: u8 = 2; - pub const SSH_MSG_UNIMPLEMENTED: u8 = 3; - pub const SSH_MSG_DEBUG: u8 = 4; - pub const SSH_MSG_SERVICE_REQUEST: u8 = 5; - pub const SSH_MSG_SERVICE_ACCEPT: u8 = 6; - pub const SSH_MSG_KEXINIT: u8 = 20; - pub const SSH_MSG_NEWKEYS: u8 = 21; - pub const SSH_MSG_KEXDH_INIT: u8 = 30; - pub const SSH_MSG_KEXDH_REPLY: u8 = 31; - pub const SSH_MSG_USERAUTH_REQUEST: u8 = 50; - pub const SSH_MSG_USERAUTH_FAILURE: u8 = 51; - pub const SSH_MSG_USERAUTH_SUCCESS: u8 = 52; - pub const SSH_MSG_USERAUTH_PK_OK: u8 = 60; - pub const SSH_MSG_GLOBAL_REQUEST: u8 = 80; - pub const SSH_MSG_REQUEST_SUCCESS: u8 = 81; - pub const SSH_MSG_REQUEST_FAILURE: u8 = 82; - pub const SSH_MSG_CHANNEL_OPEN: u8 = 90; - pub const SSH_MSG_CHANNEL_OPEN_CONFIRMATION: u8 = 91; - pub const SSH_MSG_CHANNEL_OPEN_FAILURE: u8 = 92; - pub const SSH_MSG_CHANNEL_WINDOW_ADJUST: u8 = 93; - pub const SSH_MSG_CHANNEL_DATA: u8 = 94; - pub const SSH_MSG_CHANNEL_EXTENDED_DATA: u8 = 95; - pub const SSH_MSG_CHANNEL_EOF: u8 = 96; - pub const SSH_MSG_CHANNEL_CLOSE: u8 = 97; - pub const SSH_MSG_CHANNEL_REQUEST: u8 = 98; - pub const SSH_MSG_CHANNEL_SUCCESS: u8 = 99; - pub const SSH_MSG_CHANNEL_FAILURE: u8 = 100; - - // 异常消息码 - pub const SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT: u8 = 1; - pub const SSH_DISCONNECT_PROTOCOL_ERROR: u8 = 2; - pub const SSH_DISCONNECT_KEY_EXCHANGE_FAILED: u8 = 3; - pub const SSH_DISCONNECT_RESERVED: u8 = 4; - pub const SSH_DISCONNECT_MAC_ERROR: u8 = 5; - pub const SSH_DISCONNECT_COMPRESSION_ERROR: u8 = 6; - pub const SSH_DISCONNECT_SERVICE_NOT_AVAILABLE: u8 = 7; - pub const SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED: u8 = 8; - pub const SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE: u8 = 9; - pub const SSH_DISCONNECT_CONNECTION_LOST: u8 = 10; - pub const SSH_DISCONNECT_BY_APPLICATION: u8 = 11; - pub const SSH_DISCONNECT_TOO_MANY_CONNECTIONS: u8 = 12; - pub const SSH_DISCONNECT_AUTH_CANCELLED_BY_USER: u8 = 13; - pub const SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE: u8 = 14; - pub const SSH_DISCONNECT_ILLEGAL_USER_NAME: u8 = 15; - - // 通道连接失败码 SSH_MSG_CHANNEL_OPEN_FAILURE - pub const SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: u32 = 1; - pub const SSH_OPEN_CONNECT_FAILED: u32 = 2; - pub const SSH_OPEN_UNKNOWN_CHANNEL_TYPE: u32 = 3; - pub const SSH_OPEN_RESOURCE_SHORTAGE: u32 = 4; +pub(crate) mod ssh_connection_code { + pub const GLOBAL_REQUEST: u8 = 80; + pub const REQUEST_SUCCESS: u8 = 81; + pub const REQUEST_FAILURE: u8 = 82; + pub const CHANNEL_OPEN: u8 = 90; + pub const CHANNEL_OPEN_CONFIRMATION: u8 = 91; + pub const CHANNEL_OPEN_FAILURE: u8 = 92; + pub const CHANNEL_WINDOW_ADJUST: u8 = 93; + pub const CHANNEL_DATA: u8 = 94; + pub const CHANNEL_EXTENDED_DATA: u8 = 95; + pub const CHANNEL_EOF: u8 = 96; + pub const CHANNEL_CLOSE: u8 = 97; + pub const CHANNEL_REQUEST: u8 = 98; + pub const CHANNEL_SUCCESS: u8 = 99; + pub const CHANNEL_FAILURE: u8 = 100; } -/// sftp 消息码 -/// +/// #[allow(dead_code)] -pub(crate) mod sftp_msg_code { - // 客户端初始化 - pub const SSH_FXP_INIT: u8 = 1; - // 服务器初始化 - pub const SSH_FXP_VERSION: u8 = 2; - // 打开文件包 - pub const SSH_FXP_OPEN: u8 = 3; - // 关闭句柄 - pub const SSH_FXP_CLOSE: u8 = 4; - // 读取文件 - pub const SSH_FXP_READ: u8 = 5; - // 写入文件 - pub const SSH_FXP_WRITE: u8 = 6; - // 获取文件属性 - 根据文件路径 - pub const SSH_FXP_LSTAT: u8 = 7; - // 获取文件属性 - 根据文件句柄 - pub const SSH_FXP_FSTAT: u8 = 8; - // 设置文件属性 - 根据文件路径 - pub const SSH_FXP_SETSTAT: u8 = 9; - // 设置文件属性 - 根据文件句柄 - pub const SSH_FXP_FSETSTAT: u8 = 10; - // 打开目录 - pub const SSH_FXP_OPENDIR: u8 = 11; - // 读取目录 - pub const SSH_FXP_READDIR: u8 = 12; - // 删除文件 - pub const SSH_FXP_REMOVE: u8 = 13; - // 创建目录 - pub const SSH_FXP_MKDIR: u8 = 14; - // 删除目录 - pub const SSH_FXP_RMDIR: u8 = 15; - // 相对路径转为绝对路径 - pub const SSH_FXP_REALPATH: u8 = 16; - // 同 SSH_FXP_LSTAT - // SH_FXP_STAT遵循在服务器上的符号链接,然而SSH_FXP_LSTAT不遵循符号链接 - pub const SSH_FXP_STAT: u8 = 17; - // 重命名文件 - pub const SSH_FXP_RENAME: u8 = 18; - // 读取符号链接的目标 - pub const SSH_FXP_READLINK: u8 = 19; - // 请求创建链接 - pub const SSH_FXP_LINK: u8 = 21; - // 在指定的文件上创建字节范围锁 - pub const SSH_FXP_BLOCK: u8 = 22; - // 移除之前获取的字节范围锁 - pub const SSH_FXP_UNBLOCK: u8 = 23; - // 响应状态 - pub const SSH_FXP_STATUS: u8 = 101; - // 句柄响应 - pub const SSH_FXP_HANDLE: u8 = 102; - // 数据响应 - pub const SSH_FXP_DATA: u8 = 103; - // 名称响应 - pub const SSH_FXP_NAME: u8 = 104; - // 属性响应 - pub const SSH_FXP_ATTRS: u8 = 105; - - // 拓展 - pub const SSH_FXP_EXTENDED: u8 = 200; - pub const SSH_FXP_EXTENDED_REPLY: u8 = 201; +pub(crate) mod ssh_channel_fail_code { + pub const ADMINISTRATIVELY_PROHIBITED: u32 = 1; + pub const CONNECT_FAILED: u32 = 2; + pub const UNKNOWN_CHANNEL_TYPE: u32 = 3; + pub const RESOURCE_SHORTAGE: u32 = 4; } -/// sftp 状态码 -/// +/// #[allow(dead_code)] -pub(crate) mod sftp_msg_status_code { - // 操作成功完成 - pub const SSH_FX_OK: u8 = 0; - // 读取超长/没有更多的返回目录项 - pub const SSH_FX_EOF: u8 = 1; - // 引用了一个不存在的文件 - pub const SSH_FX_NO_SUCH_FILE: u8 = 2; - // 用户权限不足 - pub const SSH_FX_PERMISSION_DENIED: u8 = 3; - // 返回错误但是不存在特定错误码 - pub const SSH_FX_FAILURE: u8 = 4; - // 数据包格式错误或者协议不兼容 - pub const SSH_FX_BAD_MESSAGE: u8 = 5; - // 没有连接到服务器(只能本地返回此错误) - pub const SSH_FX_NO_CONNECTION: u8 = 6; - // 与服务器连接丢失(只能本地返回此错误) - pub const SSH_FX_CONNECTION_LOST: u8 = 7; - // 服务器不支持该操作 - pub const SSH_FX_OP_UNSUPPORTED: u8 = 8; - // 句柄值无效 - pub const SSH_FX_INVALID_HANDLE: u8 = 9; - // 文件路径不存在或者无效 - pub const SSH_FX_NO_SUCH_PATH: u8 = 10; - // 文件已存在 - pub const SSH_FX_FILE_ALREADY_EXISTS: u8 = 11; - // 文件处于只读/保护状态 - pub const SSH_FX_WRITE_PROTECT: u8 = 12; - // 无法完成指定的操作-驱动器中没有可用的介质 - pub const SSH_FX_NO_MEDIA: u8 = 13; - // 无法完成指定的操作-文件系统上可用空间不足 - pub const SSH_FX_NO_SPACE_ON_FILESYSTEM: u8 = 14; - // 超过用户的储存配额 - pub const SSH_FX_QUOTA_EXCEEDED: u8 = 15; - // 未知的请求引用主题 - pub const SSH_FX_UNKNOWN_PRINCIPAL: u8 = 16; - // 文件被锁定,无法打开 - pub const SSH_FX_LOCK_CONFLICT: u8 = 17; - // 目录不为空 - pub const SSH_FX_DIR_NOT_EMPTY: u8 = 18; - // 指定的文件不是目录 - pub const SSH_FX_NOT_A_DIRECTORY: u8 = 19; - // 文件名无效 - pub const SSH_FX_INVALID_FILENAME: u8 = 20; - // 过多的符号链接 - pub const SSH_FX_LINK_LOOP: u8 = 21; - // 文件无法删除 - pub const SSH_FX_CANNOT_DELETE: u8 = 22; - // 参数超出范围/多参数不能以前使用 - pub const SSH_FX_INVALID_PARAMETER: u8 = 23; - // 指定的目录是上下文的目录,某些目录无法使用 - pub const SSH_FX_FILE_IS_A_DIRECTORY: u8 = 24; - // 读取或写入失败,另一个进程的字节范围锁与请求重叠 - pub const SSH_FX_BYTE_RANGE_LOCK_CONFLICT: u8 = 25; - // 字节范围锁请求被拒绝 - pub const SSH_FX_BYTE_RANGE_LOCK_REFUSED: u8 = 26; - // 删除文件操作被挂起 - pub const SSH_FX_DELETE_PENDING: u8 = 27; - // 文件已损坏 - pub const SSH_FX_FILE_CORRUPT: u8 = 28; - // 无法将指定的主体指派为文件的所有者 - pub const SSH_FX_OWNER_INVALID: u8 = 29; - // 无法将指定的主体分配为文件的主要组 - pub const SSH_FX_GROUP_INVALID: u8 = 30; - // 无法完成请求操作,因为尚未授权指定的字节范围锁定 - pub const SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK: u8 = 31; +pub(crate) mod ssh_transport_code { + pub const DISCONNECT: u8 = 1; + pub const IGNORE: u8 = 2; + pub const UNIMPLEMENTED: u8 = 3; + pub const DEBUG: u8 = 4; + pub const SERVICE_REQUEST: u8 = 5; + pub const SERVICE_ACCEPT: u8 = 6; + pub const KEXINIT: u8 = 20; + pub const NEWKEYS: u8 = 21; + pub const KEXDH_INIT: u8 = 30; + pub const KEXDH_REPLY: u8 = 31; } -/// file flags -/// +/// #[allow(dead_code)] -pub(crate) mod sftp_file_flags { - // 读取文件 - /// Open the file for reading. - /// - pub const SSH_FXF_READ: u32 = 0x00000001; - - // 写入文件 - /// Open the file for writing. - /// If both this and SSH_FXF_READ are specified, the file is opened for both reading and writing. - /// - pub const SSH_FXF_WRITE: u32 = 0x00000002; - - // 追加内容到文件 - /// Force all writes to append data at the end of the file. - /// The offset parameter to write will be ignored. - /// - pub const SSH_FXF_APPEND: u32 = 0x00000004; - - // 创建文件 - /// If this flag is specified, then a new file will be created if one - /// does not already exist (if O_TRUNC is specified, the new file will - /// be truncated to zero length if it previously exists). - /// - pub const SSH_FXF_CREAT: u32 = 0x00000008; - - // 截断文件 - /// Forces an existing file with the same name to be truncated to zero - /// length when creating a file by specifying SSH_FXF_CREAT. - /// SSH_FXF_CREAT MUST also be specified if this flag is used. - /// - pub const SSH_FXF_TRUNC: u32 = 0x00000010; - - // 创建文件时,不能有旧文件 - /// Causes the request to fail if the named file already exists. - /// SSH_FXF_CREAT MUST also be specified if this flag is used. - /// - pub const SSH_FXF_EXCL: u32 = 0x00000020; +pub(crate) mod ssh_disconnection_code { + pub const HOST_NOT_ALLOWED_TO_CONNECT: u8 = 1; + pub const PROTOCOL_ERROR: u8 = 2; + pub const KEY_EXCHANGE_FAILED: u8 = 3; + pub const RESERVED: u8 = 4; + pub const MAC_ERROR: u8 = 5; + pub const COMPRESSION_ERROR: u8 = 6; + pub const SERVICE_NOT_AVAILABLE: u8 = 7; + pub const PROTOCOL_VERSION_NOT_SUPPORTED: u8 = 8; + pub const HOST_KEY_NOT_VERIFIABLE: u8 = 9; + pub const CONNECTION_LOST: u8 = 10; + pub const BY_APPLICATION: u8 = 11; + pub const TOO_MANY_CONNECTIONS: u8 = 12; + pub const AUTH_CANCELLED_BY_USER: u8 = 13; + pub const NO_MORE_AUTH_METHODS_AVAILABLE: u8 = 14; + pub const ILLEGAL_USER_NAME: u8 = 15; +} - // 应将文件视为文本 - /// Indicates that the server should treat the file as text and - /// convert it to the canonical newline convention in use. - /// - pub const SSH_FXF_TEXT: u32 = 0x00000040; +/// +#[allow(dead_code)] +pub(crate) mod ssh_user_auth_code { + pub const REQUEST: u8 = 50; + pub const FAILURE: u8 = 51; + pub const SUCCESS: u8 = 52; + pub const BANNER: u8 = 53; + pub const PK_OK: u8 = 60; } -/// 密钥交换后进行HASH时候需要的常量值 +/// The magic that used when doing hash after kex pub(crate) const ALPHABET: [u8; 6] = [b'A', b'B', b'C', b'D', b'E', b'F']; diff --git a/src/error.rs b/src/error.rs index 320fb5f..8311c48 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,117 +1,56 @@ -use std::{ - error::Error, - fmt::{self, Debug, Display, Formatter}, - io, - sync::mpsc::{RecvError, SendError}, -}; +use std::sync::mpsc::{RecvError, SendError}; -pub type SshResult = Result; - -pub struct SshError { - inner: SshErrorKind, -} - -impl SshError { - pub fn kind(&self) -> &SshErrorKind { - &self.inner - } -} - -impl Debug for SshError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match &self.inner { - SshErrorKind::IoError(ie) => { - write!(f, r"IoError: {{ Kind({:?}), Message({}) }}", ie.kind(), ie) - } - _ => { - write!( - f, - r"Error: {{ Kind({:?}), Message({}) }}", - self.inner, self.inner - ) - } - } - } -} - -impl Display for SshError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match &self.inner { - SshErrorKind::IoError(ie) => { - write!(f, r"IoError: {{ Kind({:?}) }}", ie.kind()) - } - _ => { - write!(f, r"Error: {{ Kind({:?}) }}", self.inner) - } - } - } -} - -#[derive(Debug)] -pub enum SshErrorKind { - IoError(io::Error), - SshError(String), - SendError(String), - RecvError(String), - Timeout, -} - -impl fmt::Display for SshErrorKind { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self { - SshErrorKind::SshError(e) => write!(f, "{}", e), - SshErrorKind::IoError(v) => write!(f, "{}", v), - SshErrorKind::SendError(e) => write!(f, "{}", e), - SshErrorKind::RecvError(e) => write!(f, "{}", e), - SshErrorKind::Timeout => write!(f, "time out."), - } - } -} - -impl Error for SshError {} - -impl From for SshError { - fn from(kind: SshErrorKind) -> SshError { - SshError { inner: kind } - } -} +use thiserror::Error; -impl From<&str> for SshError { - fn from(kind: &str) -> SshError { - SshError { - inner: SshErrorKind::SshError(kind.to_string()), - } - } -} +pub type SshResult = Result; -impl From for SshError { - fn from(kind: String) -> SshError { - SshError { - inner: SshErrorKind::SshError(kind), - } - } +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum SshError { + #[error("Version dismatch: {our} vs {their}")] + VersionDismatchError { our: String, their: String }, + #[error("Key exchange error: {0}")] + KexError(String), + #[error("Parse ssh key error: {0}")] + SshPubKeyError(String), + #[error("Auth error")] + AuthError, + #[error("Timeout")] + TimeoutError, + #[error(transparent)] + DataFormatError(#[from] std::string::FromUtf8Error), + #[error("Encryption error: {0}")] + EncryptionError(String), + #[error("Compression error: {0}")] + CompressionError(String), + #[cfg(feature = "scp")] + #[error(transparent)] + SystemTimeError(#[from] std::time::SystemTimeError), + #[cfg(feature = "scp")] + #[error(transparent)] + ParseIntError(#[from] std::num::ParseIntError), + #[cfg(feature = "scp")] + #[error("Invalid scp file path")] + InvalidScpFilePath, + #[cfg(feature = "scp")] + #[error("Scp error: {0}")] + ScpError(String), + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error("IPC error: {0}")] + IpcError(String), + #[error("Ssh Error: {0}")] + GeneralError(String), } -impl From for SshError { - fn from(kind: io::Error) -> Self { - SshError { - inner: SshErrorKind::IoError(io::Error::from(kind.kind())), - } +impl From for SshError { + fn from(value: RecvError) -> Self { + Self::IpcError(value.to_string()) } } impl From> for SshError { - fn from(e: SendError) -> Self { - Self { - inner: SshErrorKind::SendError(e.to_string()), - } - } -} - -impl From for SshError { - fn from(e: RecvError) -> Self { - Self { - inner: SshErrorKind::RecvError(e.to_string()), - } + fn from(value: SendError) -> Self { + Self::IpcError(value.to_string()) } } diff --git a/src/lib.rs b/src/lib.rs index f905ee9..5d52aa6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,13 @@ //! Dependencies //! ```toml -//! ssh-rs = "0.3.2" +//! ssh-rs = "0.4.5" //! ``` //! //!Rust implementation of ssh2.0 client. //! //! Basic usage //! ```no_run -//! use ssh_rs::ssh; -//! -//! ssh::debug(); +//! use ssh; //! //! let mut session = ssh::create_session() //! .username("ubuntu") @@ -37,43 +35,25 @@ mod config; mod constant; mod model; mod session; -mod slog; mod util; pub mod error; mod sftp; pub use channel::*; -pub(crate) use error::SshError; -pub use error::{SshErrorKind, SshResult}; +pub use error::SshError; +pub use error::SshResult; pub use model::{TerminalSize, TerminalSizeType}; pub use session::{LocalSession, SessionBroker, SessionBuilder, SessionConnector}; -pub mod ssh { - use crate::{session::SessionBuilder, slog::Slog}; - - /// create a session via session builder w/ default configuration - /// - pub fn create_session() -> SessionBuilder { - SessionBuilder::new() - } - - /// create a session via session builder w/o default configuration - /// - pub fn create_session_without_default() -> SessionBuilder { - SessionBuilder::disable_default() - } - - /// set the global log level to `INFO` - /// - pub fn enable_log() { - Slog::default() - } +/// create a session via session builder w/ default configuration +/// +pub fn create_session() -> SessionBuilder { + SessionBuilder::new() +} - /// set the global log level to `TRACE` - /// - /// for diagnostic purposes only - pub fn debug() { - Slog::debug() - } +/// create a session via session builder w/o default configuration +/// +pub fn create_session_without_default() -> SessionBuilder { + SessionBuilder::disable_default() } diff --git a/src/model/data.rs b/src/model/data.rs index 09e46f1..886f194 100644 --- a/src/model/data.rs +++ b/src/model/data.rs @@ -4,45 +4,98 @@ use crate::error::SshResult; use super::Packet; -/// **byte** -/// byte 标识任意一个 8 位值(8 位字节)。固定长度的数据有时被表示为一个字节数组,写 -/// 作 byte[[n]],其中 n 是数组中字节的数量。 +/// Data Type Representations Used in the SSH Protocols +/// + +/// byte +/// +/// A byte represents an arbitrary 8-bit value (octet). Fixed length +/// data is sometimes represented as an array of bytes, written +/// byte[n], where n is the number of bytes in the array. /// /// **boolean** -/// 一个布尔值作为一个字节存储。0 表示 FALSE,1 表示 TRUE。所有非零的值必须被解释为 -/// TRUE;但是,应用软件禁止储存除 0 和 1 以外的值。 +/// +/// A boolean value is stored as a single byte. The value 0 +/// represents FALSE, and the value 1 represents TRUE. All non-zero +/// values MUST be interpreted as TRUE; however, applications MUST NOT +/// store values other than 0 and 1. /// /// **uint32** -/// 表示一个 32 位无符号整数。按重要性降序(网络字节顺序)储存为 4 个字节。 +/// +/// Represents a 32-bit unsigned integer. Stored as four bytes in the +/// order of decreasing significance (network byte order). For +/// example: the value 699921578 (0x29b7f4aa) is stored as 29 b7 f4 +/// aa. /// /// **uint64** -/// 表示一个 64 位无符号整数。按重要性降序(网络字节顺序)储存为 8 个字节。 +/// +/// Represents a 64-bit unsigned integer. Stored as eight bytes in +/// the order of decreasing significance (network byte order). /// /// **string** -/// 任意长度二进制字符串。字符串用于装载任意二进制数据,包括空字符和 8 位字符。字符 -/// 串被储存为 1 个包含其长度(后续字节数量)的 uint32 以及 0(=空字符串)或作为字符 -/// 串的值的更多的字节。不使用终结符(空字符)。 -/// 字符串也被用来存储文本。在这种情况下,内部名称使用 US-ASCII,可能显示给用户的 -/// 文本使用 ISO-10646 UTF-8。终结符(空字符)一般不应被保存在字符串中。例如, -/// US-ASCII 字符串”testing”被表示为 00 00 00 07 t e s t i n g。UTF-8 映射不 -/// 改变 US-ASCII 字符的编码。 +/// +/// Arbitrary length binary string. Strings are allowed to contain +/// arbitrary binary data, including null characters and 8-bit +/// characters. They are stored as a uint32 containing its length +/// (number of bytes that follow) and zero (= empty string) or more +/// bytes that are the value of the string. Terminating null +/// characters are not used. +/// +/// Strings are also used to store text. In that case, US-ASCII is +/// used for internal names, and ISO-10646 UTF-8 for text that might +/// be displayed to the user. The terminating null character SHOULD +/// NOT normally be stored in the string. For example: the US-ASCII +/// string "testing" is represented as 00 00 00 07 t e s t i n g. The +/// UTF-8 mapping does not alter the encoding of US-ASCII characters. /// /// **mpint** -/// 表示二进制补码(two’s complement)格式的多精度整数,存储为一个字符串,每字节 -/// 8 位,从高位到低位(MSB first)。负数的数据区的首字节的最高位(the most -/// significant bit)的值为 1。对于正数,如果最高位将被置为 1,则必须在前面加一个 -/// 值为 0 的字节。禁止包含值为 0 或 255 的非必要的前导字节(leading bytes)。零必 -/// 须被存储为具有 0 个字节的数据的字符串。 +/// +/// Represents multiple precision integers in two's complement format, +/// stored as a string, 8 bits per byte, MSB first. Negative numbers +/// have the value 1 as the most significant bit of the first byte of +/// the data partition. If the most significant bit would be set for +/// a positive number, the number MUST be preceded by a zero byte. +/// Unnecessary leading bytes with the value 0 or 255 MUST NOT be +/// included. The value zero MUST be stored as a string with zero +/// bytes of data. +/// +/// By convention, a number that is used in modular computations in +/// Z_n SHOULD be represented in the range 0 <= x < n. +/// +/// Examples: +/// +/// value (hex) representation (hex) +/// ----------- -------------------- +/// 0 00 00 00 00 +/// 9a378f9b2e332a7 00 00 00 08 09 a3 78 f9 b2 e3 32 a7 +/// 80 00 00 00 02 00 80 +/// -1234 00 00 00 02 ed cc +/// -deadbeef 00 00 00 05 ff 21 52 41 11 /// /// **name-list** -/// 一个包含逗号分隔的名称列表的字符串。名称列表表示为一个含有其长度(后续字节数量) -/// 的 uint32,加上一个包含 0 或多个逗号分隔的名称的列表。名称的长度禁止为 0,并且禁 -/// 止包含逗号(",")。由于这是一个名称列表,所有被包含的元素都是名称并且必须使用 -/// US-ASCII。上下文可能对名称有附加的限制。例如,名称列表中的名称可能必须是一系列 -/// 有效的算法标识,或一系列[[RFC3066]]语言标识。名称列表中名称的顺序可能有也可能没 -/// 有意义。这取决于使用列表时的上下文。对单个名称或整个列表都禁止使用终结字符(空 -/// 字符)。 /// +/// A string containing a comma-separated list of names. A name-list +/// is represented as a uint32 containing its length (number of bytes +/// that follow) followed by a comma-separated list of zero or more +/// names. A name MUST have a non-zero length, and it MUST NOT +/// contain a comma (","). As this is a list of names, all of the +/// elements contained are names and MUST be in US-ASCII. Context may +/// impose additional restrictions on the names. For example, the +/// names in a name-list may have to be a list of valid algorithm +/// identifiers (see Section 6 below), or a list of [RFC3066] language +/// tags. The order of the names in a name-list may or may not be +/// significant. Again, this depends on the context in which the list +/// is used. Terminating null characters MUST NOT be used, neither +/// for the individual names, nor for the list as a whole. +/// +/// Examples: +/// +/// value representation (hex) +/// ----- -------------------- +/// (), the empty name-list 00 00 00 00 +/// ("zlib") 00 00 00 04 7a 6c 69 62 +/// ("zlib,none") 00 00 00 09 7a 6c 69 62 2c 6e 6f 6e 65 + #[derive(Debug, Clone)] pub(crate) struct Data(Vec); @@ -64,21 +117,20 @@ impl Data { Data(v) } - // 无符号字节 8位 + // write uint8 pub fn put_u8(&mut self, v: u8) -> &mut Self { self.0.push(v); self } - // 32位无符号整型 + // write uint32 pub fn put_u32(&mut self, v: u32) -> &mut Self { let vec = v.to_be_bytes().to_vec(); self.0.extend(&vec); self } - // 字符串型数据 - // 需要计算字符串长度 + // write string pub fn put_str(&mut self, str: &str) -> &mut Self { let v = str.as_bytes(); self.put_u32(v.len() as u32); @@ -86,20 +138,14 @@ impl Data { self } - // 字节数组 - // 需要计算数组长度 + // write [bytes] pub fn put_u8s(&mut self, v: &[u8]) -> &mut Self { self.put_u32(v.len() as u32); self.0.extend(v); self } - // 表示二进制补码(two’s complement)格式的多精度整数 - // 存储为一个字符串,每字节8 位,从高位到低位(MSB first)。 - // 负数的数据区的首字节的最高位(the most significant bit)的值为 1。 - // 对于正数,如果最高位将被置为 1,则必须在前面加一个值为 0 的字节。 - // 禁止包含值为 0 或 255 的非必要的前导字节(leading bytes)。 - // 零必须被存储为具有 0 个字节的数据的字符串。 + // write mpint pub fn put_mpint(&mut self, v: &[u8]) -> Vec { let mut result: Vec = Vec::new(); // 0x80 = 128 @@ -110,26 +156,26 @@ impl Data { self.put_u8s(&result).to_vec() } - // 跳过多少位数据 + // skip `size` pub fn skip(&mut self, size: usize) { self.0.drain(..size); } - // 获取字节 + // get uint8 pub fn get_u8(&mut self) -> u8 { self.0.remove(0) } - // 获取32位无符号整型 + // get uint32 pub fn get_u32(&mut self) -> u32 { - let u32_buf = self.0.drain(..4).into_iter().collect::>(); + let u32_buf = self.0.drain(..4).collect::>(); u32::from_be_bytes(u32_buf.try_into().unwrap()) } - // 获取字节数组 + // get [bytes] pub fn get_u8s(&mut self) -> Vec { let len = self.get_u32() as usize; - let bytes = self.0.drain(..len).into_iter().collect::>(); + let bytes = self.0.drain(..len).collect::>(); bytes } diff --git a/src/model/flow_control.rs b/src/model/flow_control.rs index 8f1ab34..eba9143 100644 --- a/src/model/flow_control.rs +++ b/src/model/flow_control.rs @@ -3,9 +3,7 @@ use crate::constant::size::LOCAL_WINDOW_SIZE; use crate::constant::size; pub(crate) struct FlowControl { - /// 本地窗口大小 local_window: u32, - /// 远程窗口大小 remote_window: u32, } @@ -24,7 +22,7 @@ impl FlowControl { self.local_window -= recv_len; } else { let drop_len = recv_len - self.local_window; - log::debug!("Recv more than expected, drop len {}", drop_len); + tracing::debug!("Recv more than expected, drop len {}", drop_len); buf.truncate(self.local_window as usize); self.local_window = 0; } diff --git a/src/model/mod.rs b/src/model/mod.rs index 2b84575..9c830fd 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -2,12 +2,14 @@ mod backend_msg; mod data; mod flow_control; mod packet; -mod scp_file; mod sequence; mod terminal; mod timeout; mod u32iter; +#[cfg(feature = "scp")] +mod scp_file; + use std::{ cell::RefCell, rc::Rc, @@ -20,10 +22,12 @@ pub(crate) use backend_msg::*; pub(crate) use data::Data; pub(crate) use flow_control::FlowControl; pub(crate) use packet::{Packet, SecPacket}; -pub(crate) use scp_file::ScpFile; pub(crate) use sequence::Sequence; pub(crate) use timeout::Timeout; pub(crate) use u32iter::U32Iter; +#[cfg(feature = "scp")] +pub(crate) use scp_file::ScpFile; + pub(crate) type RcMut = Rc>; pub(crate) type ArcMut = Arc>; diff --git a/src/model/packet.rs b/src/model/packet.rs index 69fb818..da57c8a 100644 --- a/src/model/packet.rs +++ b/src/model/packet.rs @@ -1,46 +1,55 @@ use std::io::{Read, Write}; +use std::time::Duration; use crate::error::SshResult; use crate::{client::Client, model::Data}; use super::timeout::Timeout; -/// ## 数据包整体结构 +/// ## Binary Packet Protocol /// -/// ### uint32 `packet_length` +/// /// -/// ### byte `padding_length` +/// uint32 `packet_length` /// -/// ### byte[[n1]] `payload`; n1 = packet_length - padding_length - 1 +/// byte `padding_length` /// -/// ### byte[[n2]] `random padding`; n2 = padding_length +/// byte[[n1]] `payload`; n1 = packet_length - padding_length - 1 /// -/// ### byte[[m]] `mac` (Message Authentication Code - MAC); m = mac_length +/// byte[[n2]] `random padding`; n2 = padding_length +/// +/// byte[[m]] `mac` (Message Authentication Code - MAC); m = mac_length /// /// --- /// /// **packet_length** -/// 以字节为单位的`数据包长度`,不包括`mac`或`packet_length`域自身。 +/// The length of the packet in bytes, not including 'mac' or the 'packet_length' field itself. /// /// /// **padding_length** -/// `random padding`的长度(字节)。 +/// Length of 'random padding' (bytes). /// /// /// **payload** -/// 数据包中有用的内容。如果已经协商了压缩,该域是压缩的。初始时,压缩必须为"none"。 +/// The useful contents of the packet. If compression has been negotiated, this field is compressed. +/// Initially, compression MUST be "none". /// /// /// **random padding** -/// 任意长度的填充,使(packet_length || padding_length || payload || random padding)的总长度是加密分组长度或 8 中较大者的倍数。 -/// 最少必须有 4 字节的填充。 -/// 填充应包含随机字节。填充的最大长度为 255 字节。 -/// +/// Arbitrary-length padding, such that the total length of +/// (packet_length || padding_length || payload || random padding) +/// is a multiple of the cipher block size or 8, whichever is +/// larger. There MUST be at least four bytes of padding. The +/// padding SHOULD consist of random bytes. The maximum amount of +/// padding is 255 bytes. + /// /// **mac** -/// 消息验证码。如果已经协商了消息验证,该域包含 MAC。初始时,MAC 算法必须是"none"。 +/// Message Authentication Code. If message authentication has +/// been negotiated, this field contains the MAC bytes. Initially, +/// the MAC algorithm MUST be "none".。 -fn read_with_timeout(stream: &mut S, tm: u128, buf: &mut [u8]) -> SshResult<()> +fn read_with_timeout(stream: &mut S, tm: Option, buf: &mut [u8]) -> SshResult<()> where S: Read, { @@ -61,7 +70,7 @@ where } Err(e) => { if let std::io::ErrorKind::WouldBlock = e.kind() { - timeout.test()?; + timeout.till_next_tick()?; continue; } else { return Err(e.into()); @@ -71,7 +80,7 @@ where } } -fn try_read(stream: &mut S, _tm: u128, buf: &mut [u8]) -> SshResult +fn try_read(stream: &mut S, _tm: Option, buf: &mut [u8]) -> SshResult where S: Read, { @@ -87,7 +96,7 @@ where } } -fn write_with_timeout(stream: &mut S, tm: u128, buf: &[u8]) -> SshResult<()> +fn write_with_timeout(stream: &mut S, tm: Option, buf: &[u8]) -> SshResult<()> where S: Write, { @@ -108,7 +117,7 @@ where } Err(e) => { if let std::io::ErrorKind::WouldBlock = e.kind() { - timeout.test()?; + timeout.till_next_tick()?; continue; } else { return Err(e.into()); @@ -131,31 +140,35 @@ pub(crate) struct SecPacket<'a> { } impl<'a> SecPacket<'a> { + fn get_align(bsize: usize) -> i32 { + let bsize = bsize as i32; + if bsize > 8 { + bsize + } else { + 8 + } + } + pub fn write_stream(self, stream: &mut S) -> SshResult<()> where S: Write, { let tm = self.client.get_timeout(); - let payload_len = self.payload.len() as u32; - let group_size = self.client.get_encryptor().group_size() as i32; + let payload = self.client.get_compressor().compress(&self.payload)?; + let payload_len = payload.len() as u32; let pad_len = { - let mut pad = payload_len as i32; - if self.client.get_encryptor().is_cp() { - pad += 1; - } else { - pad += 5 + let mut pad = payload_len as i32 + 1; + let block_size = Self::get_align(self.client.get_encryptor().bsize()); + if !self.client.get_encryptor().no_pad() { + pad += 4 } - pad = (-pad) & (group_size as i32 - 1); - if pad < group_size { - pad += group_size; - } - pad as u32 + (((-pad) & (block_size - 1)) + block_size) as u32 } as u8; let packet_len = 1 + pad_len as u32 + payload_len; let mut buf = vec![]; buf.extend(packet_len.to_be_bytes()); buf.extend([pad_len]); - buf.extend(self.payload.iter()); + buf.extend(payload); buf.extend(vec![0; pad_len as usize]); let seq = self.client.get_seq().get_client(); self.client.get_encryptor().encrypt(seq, &mut buf); @@ -167,14 +180,7 @@ impl<'a> SecPacket<'a> { S: Read, { let tm = client.get_timeout(); - let bsize = { - let bsize = client.get_encryptor().bsize(); - if bsize > 8 { - bsize - } else { - 8 - } - }; + let bsize = Self::get_align(client.get_encryptor().bsize()) as usize; // read the first block let mut first_block = vec![0; bsize]; @@ -198,6 +204,7 @@ impl<'a> SecPacket<'a> { let payload_len = pkt_len - pad_len as u32 - 1; let payload = data[5..payload_len as usize + 5].into(); + let payload = client.get_compressor().decompress(payload)?.into(); Ok(Self { payload, client }) } @@ -207,14 +214,7 @@ impl<'a> SecPacket<'a> { S: Read, { let tm = client.get_timeout(); - let bsize = { - let bsize = client.get_encryptor().bsize(); - if bsize > 8 { - bsize - } else { - 8 - } - }; + let bsize = Self::get_align(client.get_encryptor().bsize()) as usize; // read the first block let mut first_block = vec![0; bsize]; diff --git a/src/model/timeout.rs b/src/model/timeout.rs index 7a960e4..1d1fc99 100644 --- a/src/model/timeout.rs +++ b/src/model/timeout.rs @@ -1,27 +1,47 @@ -use crate::error::SshErrorKind; -use crate::{slog::log, SshError, SshResult}; -use std::time::Instant; +use crate::{SshError, SshResult}; +use std::time::{Duration, Instant}; + +#[cfg(not(target_arch = "wasm32"))] +const NANOS_PER_SEC: u64 = 1_000_000_000; pub(crate) struct Timeout { instant: Instant, - timeout_millisec: u128, + timeout: Option, + wait_tick: u64, } impl Timeout { - pub fn new(timeout_millisec: u128) -> Self { + pub fn new(timeout: Option) -> Self { Timeout { instant: Instant::now(), - timeout_millisec, + timeout, + wait_tick: 1, } } - pub fn test(&self) -> SshResult<()> { - if self.timeout_millisec == 0 { - Ok(()) - } else if self.instant.elapsed().as_millis() > self.timeout_millisec { - log::error!("time out."); - Err(SshError::from(SshErrorKind::Timeout)) + fn wait(&mut self) -> u64 { + #[cfg(not(target_arch = "wasm32"))] + { + let sleep_time = Duration::from_nanos(self.wait_tick); + std::thread::sleep(sleep_time); + if self.wait_tick < NANOS_PER_SEC { + self.wait_tick <<= 1; + } + } + self.wait_tick + } + + pub fn till_next_tick(&mut self) -> SshResult<()> { + if let Some(t) = self.timeout { + if self.instant.elapsed() > t { + tracing::error!("time out."); + Err(SshError::TimeoutError) + } else { + self.wait(); + Ok(()) + } } else { + self.wait(); Ok(()) } } diff --git a/src/session/mod.rs b/src/session/mod.rs index a7cf2e7..28a2341 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -4,17 +4,19 @@ mod session_local; pub use session_broker::SessionBroker; pub use session_local::LocalSession; +use tracing::*; use std::{ io::{Read, Write}, net::{TcpStream, ToSocketAddrs}, path::Path, + time::Duration, }; use crate::{ algorithm::{Compress, Digest, Enc, Kex, Mac, PubKey}, client::Client, - config::{algorithm::AlgList, version::SshVersion, Config}, + config::{algorithm::AlgList, Config}, error::SshResult, model::{Packet, SecPacket}, }; @@ -47,15 +49,16 @@ where } .connect(), SessionState::Version(mut config, mut stream) => { - log::info!("start for version negotiation."); + info!("start for version negotiation."); + // Send Client version + config.ver.send_our_version(&mut stream)?; + // Receive the server version - let version = SshVersion::from(&mut stream, config.timeout)?; + config + .ver + .read_server_version(&mut stream, config.timeout)?; // Version validate - version.validate()?; - // Send Client version - SshVersion::write(&mut stream)?; - // Store the version info - config.ver = version; + config.ver.validate()?; // from now on // each step of the interaction is subject to the ssh constraints on the packet @@ -75,7 +78,7 @@ where digest.hash_ctx.set_i_s(server_algs.get_inner()); let server_algs = AlgList::unpack(server_algs)?; client.key_agreement(&mut stream, server_algs, &mut digest)?; - client.do_auth(&mut stream, &mut digest)?; + client.do_auth(&mut stream, &digest)?; Ok(Self { inner: SessionState::Connected(client, stream), }) @@ -138,11 +141,8 @@ impl SessionBuilder { } } - /// add a globle r/w timeout for local ssh mode - /// - /// set 0 to disable - /// - pub fn timeout(mut self, timeout: u128) -> Self { + /// Read/Write timeout for local SSH mode. Use None to disable timeout. + pub fn timeout(mut self, timeout: Option) -> Self { self.config.timeout = timeout; self } @@ -163,7 +163,7 @@ impl SessionBuilder { { match self.config.auth.private_key(private_key) { Ok(_) => (), - Err(e) => log::error!( + Err(e) => error!( "Parse private key from string: {}, will fallback to password authentication", e ), @@ -177,7 +177,7 @@ impl SessionBuilder { { match self.config.auth.private_key_path(key_path) { Ok(_) => (), - Err(e) => log::error!( + Err(e) => error!( "Parse private key from file: {}, will fallback to password authentication", e ), @@ -246,7 +246,12 @@ impl SessionBuilder { A: ToSocketAddrs, { // connect tcp by default - let tcp = TcpStream::connect(addr)?; + let tcp = if let Some(ref to) = self.config.timeout { + TcpStream::connect_timeout(&addr.to_socket_addrs()?.next().unwrap(), *to)? + } else { + TcpStream::connect(addr)? + }; + // default nonblocking tcp.set_nonblocking(true).unwrap(); self.connect_bio(tcp) diff --git a/src/session/session_broker.rs b/src/session/session_broker.rs index c3b9834..2732f87 100644 --- a/src/session/session_broker.rs +++ b/src/session/session_broker.rs @@ -8,19 +8,22 @@ use std::{ thread::spawn, }; -use log::info; +use tracing::*; use crate::{ algorithm::Digest, channel::{BackendChannel, ExecBroker}, client::Client, config::algorithm::AlgList, - constant::{size, ssh_msg_code, ssh_str}, + constant::{size, ssh_channel_fail_code, ssh_connection_code, ssh_str, ssh_transport_code}, error::{SshError, SshResult}, model::{ArcMut, BackendResp, BackendRqst, Data, Packet, SecPacket, U32Iter}, - ChannelBroker, ScpBroker, ShellBrocker, TerminalSize, + ChannelBroker, ShellBrocker, TerminalSize, }; +#[cfg(feature = "scp")] +use crate::ScpBroker; + pub struct SessionBroker { channel_num: ArcMut, snd: Sender, @@ -34,7 +37,7 @@ impl SessionBroker { let (rqst_snd, rqst_rcv) = mpsc::channel(); spawn(move || { if let Err(e) = client_loop(client, stream, rqst_rcv) { - log::error!("Error {} occurred when running backend task", e.to_string()) + error!("Error {:?} occurred when running backend task", e) } }); Self { @@ -46,7 +49,7 @@ impl SessionBroker { /// close the backend session and consume the session broker itself /// pub fn close(self) { - log::info!("Client close"); + info!("Client close"); drop(self) } @@ -59,6 +62,7 @@ impl SessionBroker { /// open a [ScpBroker] channel which can download/upload files/directories /// + #[cfg(feature = "scp")] pub fn open_scp(&mut self) -> SshResult { let channel = self.open_channel()?; channel.scp() @@ -70,7 +74,7 @@ impl SessionBroker { self.open_shell_terminal(TerminalSize::from(80, 24)) } - /// open a [LocalShell] channel + /// open a [ShellBrocker] channel /// /// custom terminal dimensions /// @@ -89,7 +93,7 @@ impl SessionBroker { // open channel request let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_OPEN) + data.put_u8(ssh_connection_code::CHANNEL_OPEN) .put_str(ssh_str::SESSION) .put_u32(client_id) .put_u32(size::LOCAL_WINDOW_SIZE) @@ -107,7 +111,7 @@ impl SessionBroker { resp_recv, self.snd.clone(), )), - BackendResp::Fail(msg) => Err(SshError::from(msg)), + BackendResp::Fail(msg) => Err(SshError::GeneralError(msg)), _ => unreachable!(), }, Err(e) => Err(e.into()), @@ -121,7 +125,7 @@ where { let mut channels = HashMap::::new(); let mut pendings = HashMap::>::new(); - client.set_timeout(0); + client.set_timeout(None); loop { let try_recv = rcv.try_recv(); if try_recv.is_err() { @@ -132,7 +136,7 @@ where } else if let Ok(rqst) = try_recv { match rqst { BackendRqst::OpenChannel(id, data, sender) => { - log::info!("try open channel {}.", id); + info!("try open channel {}.", id); data.pack(&mut client).write_stream(&mut stream)?; @@ -142,17 +146,17 @@ where BackendRqst::Data(id, data) => { let channel = channels.get_mut(&id).unwrap(); - log::trace!("Channel {} send {} data", id, data.len()); + trace!("Channel {} send {} data", id, data.len()); channel.send_data(data, &mut client, &mut stream)?; } BackendRqst::Command(id, data) => { let channel = channels.get_mut(&id).unwrap(); - log::trace!("Channel {} send control data", id); + trace!("Channel {} send control data", id); channel.send(data, &mut client, &mut stream)?; } BackendRqst::CloseChannel(id, data) => { - log::info!("try close channel {}.", id); + info!("try close channel {}.", id); let channel = channels.get_mut(&id).unwrap(); channel.send(data, &mut client, &mut stream)?; @@ -170,7 +174,7 @@ where match message_code { // Successfully open a channel - ssh_msg_code::SSH_MSG_CHANNEL_OPEN_CONFIRMATION => { + ssh_connection_code::CHANNEL_OPEN_CONFIRMATION => { let client_channel_no = data.get_u32(); let server_channel_no = data.get_u32(); let remote_window_size = data.get_u32(); @@ -195,14 +199,14 @@ where .is_none()) } /* - byte SSH_MSG_CHANNEL_OPEN_FAILURE + byte CHANNEL_OPEN_FAILURE uint32 recipient channel uint32 reason code - string description,ISO-10646 UTF-8 编码[RFC3629] + string description,ISO-10646 UTF-8 [RFC3629] string language tag,[RFC3066] */ // Fail to open a channel - ssh_msg_code::SSH_MSG_CHANNEL_OPEN_FAILURE => { + ssh_connection_code::CHANNEL_OPEN_FAILURE => { // client channel number let id = data.get_u32(); @@ -217,37 +221,47 @@ where data.get_u8s(); let err_msg = match code { - ssh_msg_code::SSH_OPEN_ADMINISTRATIVELY_PROHIBITED => { - format!("SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: {}", description) + ssh_channel_fail_code::ADMINISTRATIVELY_PROHIBITED => { + format!("ADMINISTRATIVELY_PROHIBITED: {description}") } - ssh_msg_code::SSH_OPEN_CONNECT_FAILED => { - format!("SSH_OPEN_CONNECT_FAILED: {}", description) + ssh_channel_fail_code::CONNECT_FAILED => { + format!("CONNECT_FAILED: {description}") } - ssh_msg_code::SSH_OPEN_UNKNOWN_CHANNEL_TYPE => { - format!("SSH_OPEN_UNKNOWN_CHANNEL_TYPE: {}", description) + ssh_channel_fail_code::UNKNOWN_CHANNEL_TYPE => { + format!("UNKNOWN_CHANNEL_TYPE: {description}") } - ssh_msg_code::SSH_OPEN_RESOURCE_SHORTAGE => { - format!("SSH_OPEN_RESOURCE_SHORTAGE: {}", description) + ssh_channel_fail_code::RESOURCE_SHORTAGE => { + format!("RESOURCE_SHORTAGE: {description}") } _ => description, }; sender.unwrap().send(BackendResp::Fail(err_msg))?; } - ssh_msg_code::SSH_MSG_KEXINIT => { + ssh_transport_code::KEXINIT => { data.insert(0, message_code); let mut digest = Digest::new(); digest.hash_ctx.set_i_s(&data); let server_algs = AlgList::unpack((data, &mut client).into())?; client.key_agreement(&mut stream, server_algs, &mut digest)?; } - ssh_msg_code::SSH_MSG_CHANNEL_DATA => { + ssh_connection_code::CHANNEL_DATA => { + let id = data.get_u32(); + trace!("Channel {id} get {} data", data.len()); + let channel = channels.get_mut(&id).unwrap(); + channel.recv(data, &mut client, &mut stream)?; + } + ssh_connection_code::CHANNEL_EXTENDED_DATA => { let id = data.get_u32(); - log::trace!("Channel {} get {} data", id, data.len()); + let data_type = data.get_u32(); + trace!( + "Channel {id} get {} extended data, type {data_type}", + data.len(), + ); let channel = channels.get_mut(&id).unwrap(); channel.recv(data, &mut client, &mut stream)?; } // flow_control msg - ssh_msg_code::SSH_MSG_CHANNEL_WINDOW_ADJUST => { + ssh_connection_code::CHANNEL_WINDOW_ADJUST => { // client channel number let id = data.get_u32(); // to_add @@ -255,43 +269,43 @@ where let channel = channels.get_mut(&id).unwrap(); channel.recv_window_adjust(rws, &mut client, &mut stream)?; } - ssh_msg_code::SSH_MSG_CHANNEL_CLOSE => { + ssh_connection_code::CHANNEL_CLOSE => { let id = data.get_u32(); - log::info!("Channel {} recv close", id); + info!("Channel {} recv close", id); let channel = channels.get_mut(&id).unwrap(); channel.remote_close()?; if channel.is_close() { channels.remove(&id); } } - ssh_msg_code::SSH_MSG_GLOBAL_REQUEST => { + ssh_connection_code::GLOBAL_REQUEST => { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_REQUEST_FAILURE); + data.put_u8(ssh_connection_code::REQUEST_FAILURE); data.pack(&mut client).write_stream(&mut stream)?; continue; } - x @ ssh_msg_code::SSH_MSG_CHANNEL_EOF => { - log::debug!("Currently ignore message {}", x); + x @ ssh_connection_code::CHANNEL_EOF => { + debug!("Currently ignore message {}", x); } - x @ ssh_msg_code::SSH_MSG_CHANNEL_REQUEST => { - log::debug!("Currently ignore message {}", x); + x @ ssh_connection_code::CHANNEL_REQUEST => { + debug!("Currently ignore message {}", x); } - _x @ ssh_msg_code::SSH_MSG_CHANNEL_SUCCESS => { + _x @ ssh_connection_code::CHANNEL_SUCCESS => { let id = data.get_u32(); - log::trace!("Channel {} control success", id); + trace!("Channel {} control success", id); let channel = channels.get_mut(&id).unwrap(); channel.success()? } - ssh_msg_code::SSH_MSG_CHANNEL_FAILURE => { + ssh_connection_code::CHANNEL_FAILURE => { let id = data.get_u32(); - log::trace!("Channel {} control failed", id); + trace!("Channel {} control failed", id); let channel = channels.get_mut(&id).unwrap(); channel.failed()? } x => { - log::debug!("Currently ignore message {}", x); + debug!("Currently ignore message {}", x); } } } diff --git a/src/session/session_local.rs b/src/session/session_local.rs index 3020a22..6e67894 100644 --- a/src/session/session_local.rs +++ b/src/session/session_local.rs @@ -2,14 +2,18 @@ use std::{ cell::RefCell, io::{Read, Write}, rc::Rc, + time::Duration, }; +use tracing::*; -use crate::model::TerminalSize; +#[cfg(feature = "scp")] +use crate::channel::LocalScp; use crate::{ - channel::{LocalChannel, LocalExec, LocalScp, LocalShell}, + channel::{LocalChannel, LocalExec, LocalShell}, client::Client, - constant::{size, ssh_msg_code, ssh_str}, + constant::{size, ssh_channel_fail_code, ssh_connection_code, ssh_str}, error::{SshError, SshResult}, + model::TerminalSize, model::{Data, Packet, RcMut, SecPacket, U32Iter}, LocalSftp, }; @@ -38,10 +42,17 @@ where /// close the local session and consume it /// pub fn close(self) { - log::info!("Client close"); + info!("Client close"); drop(self) } + /// Modify the timeout setting + /// in case the user wants to change the timeout during ssh operations. + /// + pub fn set_timeout(&mut self, timeout: Option) { + self.client.borrow_mut().set_timeout(timeout) + } + /// open a [LocalExec] channel which can excute commands /// pub fn open_exec(&mut self) -> SshResult> { @@ -51,6 +62,7 @@ where /// open a [LocalScp] channel which can download/upload files/directories /// + #[cfg(feature = "scp")] pub fn open_scp(&mut self) -> SshResult> { let channel = self.open_channel()?; channel.scp() @@ -85,7 +97,7 @@ where /// need call `.exec()`, `.shell()`, `.scp()` and so on to convert it to a specific channel /// pub fn open_channel(&mut self) -> SshResult> { - log::info!("channel opened."); + info!("channel opened."); let client_channel_no = self.channel_num.next().unwrap(); self.send_open_channel(client_channel_no)?; @@ -100,10 +112,10 @@ where )) } - // 本地请求远程打开通道 + // open channel request fn send_open_channel(&mut self, client_channel_no: u32) -> SshResult<()> { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_CHANNEL_OPEN) + data.put_u8(ssh_connection_code::CHANNEL_OPEN) .put_str(ssh_str::SESSION) .put_u32(client_channel_no) .put_u32(size::LOCAL_WINDOW_SIZE) @@ -112,7 +124,7 @@ where .write_stream(&mut *self.stream.borrow_mut()) } - // 远程回应是否可以打开通道 + // get the response of the channel request fn receive_open_channel(&mut self) -> SshResult<(u32, u32)> { loop { let mut data = Data::unpack(SecPacket::from_stream( @@ -122,62 +134,59 @@ where let message_code = data.get_u8(); match message_code { - // 打开请求通过 - ssh_msg_code::SSH_MSG_CHANNEL_OPEN_CONFIRMATION => { - // 接收方通道号 + // Successfully open a channel + ssh_connection_code::CHANNEL_OPEN_CONFIRMATION => { data.get_u32(); - // 发送方通道号 let server_channel_no = data.get_u32(); - // 远程初始窗口大小 let remote_window_size = data.get_u32(); - // 远程的最大数据包大小, 暂时不需要 + // remote packet size, currently don't need it data.get_u32(); return Ok((server_channel_no, remote_window_size)); } /* - byte SSH_MSG_CHANNEL_OPEN_FAILURE + byte CHANNEL_OPEN_FAILURE uint32 recipient channel uint32 reason code - string description,ISO-10646 UTF-8 编码[RFC3629] + string description,ISO-10646 UTF-8 [RFC3629] string language tag,[RFC3066] */ - // 打开请求拒绝 - ssh_msg_code::SSH_MSG_CHANNEL_OPEN_FAILURE => { + // Fail to open a channel + ssh_connection_code::CHANNEL_OPEN_FAILURE => { data.get_u32(); - // 失败原因码 + // error code let code = data.get_u32(); - // 消息详情 默认utf-8编码 + // error detail: By default is utf-8 let description = String::from_utf8(data.get_u8s()).unwrap_or_else(|_| String::from("error")); - // language tag 暂不处理, 应该是 en-US + // language tag, assume to be en-US data.get_u8s(); let err_msg = match code { - ssh_msg_code::SSH_OPEN_ADMINISTRATIVELY_PROHIBITED => { - format!("SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: {}", description) + ssh_channel_fail_code::ADMINISTRATIVELY_PROHIBITED => { + format!("ADMINISTRATIVELY_PROHIBITED: {}", description) } - ssh_msg_code::SSH_OPEN_CONNECT_FAILED => { - format!("SSH_OPEN_CONNECT_FAILED: {}", description) + ssh_channel_fail_code::CONNECT_FAILED => { + format!("CONNECT_FAILED: {}", description) } - ssh_msg_code::SSH_OPEN_UNKNOWN_CHANNEL_TYPE => { - format!("SSH_OPEN_UNKNOWN_CHANNEL_TYPE: {}", description) + ssh_channel_fail_code::UNKNOWN_CHANNEL_TYPE => { + format!("UNKNOWN_CHANNEL_TYPE: {}", description) } - ssh_msg_code::SSH_OPEN_RESOURCE_SHORTAGE => { - format!("SSH_OPEN_RESOURCE_SHORTAGE: {}", description) + ssh_channel_fail_code::RESOURCE_SHORTAGE => { + format!("RESOURCE_SHORTAGE: {}", description) } _ => description, }; - return Err(SshError::from(err_msg)); + return Err(SshError::GeneralError(err_msg)); } - ssh_msg_code::SSH_MSG_GLOBAL_REQUEST => { + ssh_connection_code::GLOBAL_REQUEST => { let mut data = Data::new(); - data.put_u8(ssh_msg_code::SSH_MSG_REQUEST_FAILURE); + data.put_u8(ssh_connection_code::REQUEST_FAILURE); data.pack(&mut self.client.borrow_mut()) .write_stream(&mut *self.stream.borrow_mut())?; continue; } x => { - log::debug!("Ignore ssh msg {}", x); + debug!("Ignore ssh msg {}", x); continue; } } diff --git a/src/slog.rs b/src/slog.rs deleted file mode 100644 index dc0ce40..0000000 --- a/src/slog.rs +++ /dev/null @@ -1,42 +0,0 @@ -pub use log; - -use log::{LevelFilter, Log, Metadata, Record}; - -pub(crate) static SLOG: Slog = Slog; - -pub struct Slog; - -impl Slog { - fn init(level: LevelFilter) { - if let Err(e) = log::set_logger(&SLOG) { - log::error!( - "initialization log error, the error information is: {:?}", - e - ); - return; - } - log::set_max_level(level); - } - - pub fn default() { - Slog::init(LevelFilter::Info) - } - - pub fn debug() { - Slog::init(LevelFilter::Trace) - } -} - -impl Log for Slog { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() != LevelFilter::Off - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - println!("[SSH]-[{}]: {}", record.level(), record.args()); - } - } - - fn flush(&self) {} -} diff --git a/src/util.rs b/src/util.rs index 593e8a2..604f7d4 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,32 +1,19 @@ -use crate::error::{SshError, SshResult}; -use crate::slog::log; +use crate::error::SshResult; use rand::rngs::OsRng; use rand::Rng; + +#[cfg(feature = "scp")] +use crate::error::SshError; +#[cfg(feature = "scp")] use std::{ path::Path, str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; -pub(crate) fn from_utf8(v: Vec) -> SshResult { - match String::from_utf8(v) { - Ok(v) => Ok(v), - Err(e) => { - let err_msg = format!("Byte to utf8 string error, error info: {:?}", e); - log::error!("{}", err_msg); - Err(SshError::from(err_msg)) - } - } -} - +#[cfg(feature = "scp")] pub(crate) fn sys_time_to_secs(time: SystemTime) -> SshResult { - match time.duration_since(UNIX_EPOCH) { - Ok(t) => Ok(t.as_secs()), - Err(e) => Err(SshError::from(format!( - "SystemTimeError difference: {:?}", - e.duration() - ))), - } + Ok(time.duration_since(UNIX_EPOCH)?.as_secs()) } // a random cookie @@ -36,29 +23,24 @@ pub(crate) fn cookie() -> Vec { } pub(crate) fn vec_u8_to_string(v: Vec, pat: &str) -> SshResult> { - let result = from_utf8(v)?; + let result = String::from_utf8(v)?; let r: Vec<&str> = result.split(pat).collect(); let mut vec = vec![]; for x in r { - vec.push(x.to_string()) + vec.push(x.to_owned()) } Ok(vec) } -pub(crate) fn str_to_i64(v: &str) -> SshResult { - match i64::from_str(v) { - Ok(v) => Ok(v), - Err(_) => Err(SshError::from("str to i64 error")), - } -} - +#[cfg(feature = "scp")] pub(crate) fn check_path(path: &Path) -> SshResult<()> { if path.to_str().is_none() { - return Err(SshError::from("invalid path.")); + return Err(SshError::InvalidScpFilePath); } Ok(()) } +#[cfg(feature = "scp")] pub(crate) fn file_time(v: Vec) -> SshResult<(i64, i64)> { let mut t = vec![]; for x in v { @@ -68,7 +50,7 @@ pub(crate) fn file_time(v: Vec) -> SshResult<(i64, i64)> { t.push(x) } let a = t.len() / 2; - let ct = from_utf8(t[..(a - 1)].to_vec())?; - let ut = from_utf8(t[a..(t.len() - 1)].to_vec())?; - Ok((str_to_i64(&ct)?, str_to_i64(&ut)?)) + let ct = String::from_utf8(t[..(a - 1)].to_vec())?; + let ut = String::from_utf8(t[a..(t.len() - 1)].to_vec())?; + Ok((i64::from_str(&ct)?, i64::from_str(&ut)?)) } diff --git a/tests/algorithms.rs b/tests/algorithms.rs index d706555..d86e1bc 100644 --- a/tests/algorithms.rs +++ b/tests/algorithms.rs @@ -1,6 +1,6 @@ mod test { use paste::paste; - use ssh_rs::{algorithm, ssh}; + use ssh::algorithm; use std::env; macro_rules! env_getter { @@ -15,13 +15,15 @@ mod test { env_getter!(username, "ubuntu"); env_getter!(server, "127.0.0.1:22"); env_getter!(pem_rsa, "./rsa_old"); + #[cfg(feature = "deprecated-dss-sha1")] + env_getter!(passwd, "password"); - #[cfg(feature = "dangerous-rsa-sha1")] + #[cfg(feature = "deprecated-rsa-sha1")] #[test] fn test_ssh_rsa() { let session = ssh::create_session_without_default() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .add_kex_algorithms(algorithm::Kex::EcdhSha2Nistrp256) .add_pubkey_algorithms(algorithm::PubKey::SshRsa) .add_enc_algorithms(algorithm::Enc::Chacha20Poly1305Openssh) @@ -33,14 +35,31 @@ mod test { session.close(); } - #[cfg(feature = "dangerous-algorithms")] + #[cfg(feature = "deprecated-dss-sha1")] + #[test] + fn test_ssh_dss() { + let session = ssh::create_session_without_default() + .username(&get_username()) + .password(&get_passwd()) + .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha1) + .add_pubkey_algorithms(algorithm::PubKey::SshDss) + .add_enc_algorithms(algorithm::Enc::Aes128Ctr) + .add_compress_algorithms(algorithm::Compress::None) + .add_mac_algortihms(algorithm::Mac::HmacSha1) + .connect(get_server()) + .unwrap() + .run_local(); + session.close(); + } + + #[cfg(feature = "deprecated-dh-group1-sha1")] #[test] fn test_dh_group1() { let session = ssh::create_session_without_default() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup1Sha1) - .add_pubkey_algorithms(algorithm::PubKey::SshRsa) + .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) .add_enc_algorithms(algorithm::Enc::Aes128Ctr) .add_compress_algorithms(algorithm::Compress::None) .add_mac_algortihms(algorithm::Mac::HmacSha1) @@ -54,7 +73,7 @@ mod test { fn test_curve25519_sha256() { let session = ssh::create_session_without_default() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .add_kex_algorithms(algorithm::Kex::Curve25519Sha256) .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) .add_enc_algorithms(algorithm::Enc::Aes128Ctr) @@ -70,7 +89,7 @@ mod test { fn test_ecdh_sha2_nistp256() { let session = ssh::create_session_without_default() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .add_kex_algorithms(algorithm::Kex::EcdhSha2Nistrp256) .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) .add_enc_algorithms(algorithm::Enc::Aes128Ctr) @@ -82,11 +101,12 @@ mod test { session.close(); } + #[cfg(feature = "deprecated-dh-group1-sha1")] #[test] fn test_dh_group14_sha1() { let session = ssh::create_session_without_default() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha1) .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) .add_enc_algorithms(algorithm::Enc::Aes128Ctr) @@ -102,7 +122,7 @@ mod test { fn test_dh_group14_sha256() { let session = ssh::create_session_without_default() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) .add_enc_algorithms(algorithm::Enc::Aes128Ctr) @@ -114,11 +134,111 @@ mod test { session.close(); } + #[test] + fn test_aes192_ctr() { + let session = ssh::create_session_without_default() + .username(&get_username()) + .private_key_path(get_pem_rsa()) + .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) + .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) + .add_enc_algorithms(algorithm::Enc::Aes192Ctr) + .add_compress_algorithms(algorithm::Compress::None) + .add_mac_algortihms(algorithm::Mac::HmacSha1) + .connect(get_server()) + .unwrap() + .run_local(); + session.close(); + } + + #[test] + fn test_aes256_ctr() { + let session = ssh::create_session_without_default() + .username(&get_username()) + .private_key_path(get_pem_rsa()) + .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) + .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) + .add_enc_algorithms(algorithm::Enc::Aes256Ctr) + .add_compress_algorithms(algorithm::Compress::None) + .add_mac_algortihms(algorithm::Mac::HmacSha1) + .connect(get_server()) + .unwrap() + .run_local(); + session.close(); + } + + #[cfg(feature = "deprecated-aes-cbc")] + #[test] + fn test_aes128_cbc() { + let session = ssh::create_session_without_default() + .username(&get_username()) + .private_key_path(get_pem_rsa()) + .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) + .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) + .add_enc_algorithms(algorithm::Enc::Aes128Cbc) + .add_compress_algorithms(algorithm::Compress::None) + .add_mac_algortihms(algorithm::Mac::HmacSha1) + .connect(get_server()) + .unwrap() + .run_local(); + session.close(); + } + + #[cfg(feature = "deprecated-aes-cbc")] + #[test] + fn test_aes192_cbc() { + let session = ssh::create_session_without_default() + .username(&get_username()) + .private_key_path(get_pem_rsa()) + .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) + .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) + .add_enc_algorithms(algorithm::Enc::Aes192Cbc) + .add_compress_algorithms(algorithm::Compress::None) + .add_mac_algortihms(algorithm::Mac::HmacSha1) + .connect(get_server()) + .unwrap() + .run_local(); + session.close(); + } + + #[cfg(feature = "deprecated-aes-cbc")] + #[test] + fn test_aes256_cbc() { + let session = ssh::create_session_without_default() + .username(&get_username()) + .private_key_path(get_pem_rsa()) + .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) + .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) + .add_enc_algorithms(algorithm::Enc::Aes256Cbc) + .add_compress_algorithms(algorithm::Compress::None) + .add_mac_algortihms(algorithm::Mac::HmacSha1) + .connect(get_server()) + .unwrap() + .run_local(); + session.close(); + } + + #[cfg(feature = "deprecated-des-cbc")] + #[test] + fn test_3des_cbc() { + let session = ssh::create_session_without_default() + .username(&get_username()) + .private_key_path(get_pem_rsa()) + .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) + .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) + .add_enc_algorithms(algorithm::Enc::TripleDesCbc) + .add_compress_algorithms(algorithm::Compress::None) + .add_mac_algortihms(algorithm::Mac::HmacSha1) + .connect(get_server()) + .unwrap() + .run_local(); + session.close(); + } + #[test] fn test_chacha20_poly1305() { let session = ssh::create_session_without_default() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) .add_enc_algorithms(algorithm::Enc::Chacha20Poly1305Openssh) @@ -130,11 +250,27 @@ mod test { session.close(); } + #[test] + fn test_zlib_openssh_com() { + let session = ssh::create_session_without_default() + .username(&get_username()) + .private_key_path(get_pem_rsa()) + .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) + .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) + .add_enc_algorithms(algorithm::Enc::Chacha20Poly1305Openssh) + .add_compress_algorithms(algorithm::Compress::ZlibOpenSsh) + .add_mac_algortihms(algorithm::Mac::HmacSha1) + .connect(get_server()) + .unwrap() + .run_local(); + session.close(); + } + #[test] fn test_hmac_sha256() { let session = ssh::create_session_without_default() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) .add_enc_algorithms(algorithm::Enc::Chacha20Poly1305Openssh) @@ -150,7 +286,7 @@ mod test { fn test_hmac_sha512() { let session = ssh::create_session_without_default() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .add_kex_algorithms(algorithm::Kex::DiffieHellmanGroup14Sha256) .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) .add_enc_algorithms(algorithm::Enc::Chacha20Poly1305Openssh) diff --git a/tests/connect.rs b/tests/connect.rs index 2fe0e8e..2487725 100644 --- a/tests/connect.rs +++ b/tests/connect.rs @@ -1,6 +1,6 @@ mod tests { use paste::paste; - use ssh_rs::ssh; + use std::env; macro_rules! env_getter { @@ -45,7 +45,7 @@ mod tests { fn test_rsa_old() { let session = ssh::create_session() .username(&get_username()) - .private_key_path(&get_pem_rsa()) + .private_key_path(get_pem_rsa()) .connect(get_server()) .unwrap() .run_local(); @@ -56,7 +56,7 @@ mod tests { fn test_rsa_new() { let session = ssh::create_session() .username(&get_username()) - .private_key_path(&get_openssh_rsa()) + .private_key_path(get_openssh_rsa()) .connect(get_server()) .unwrap() .run_local(); @@ -67,7 +67,7 @@ mod tests { fn test_ed25519() { let session = ssh::create_session() .username(&get_username()) - .private_key_path(&get_ed25519()) + .private_key_path(get_ed25519()) .connect(get_server()) .unwrap() .run_local(); diff --git a/tests/exec.rs b/tests/exec.rs index 5751697..ed54005 100644 --- a/tests/exec.rs +++ b/tests/exec.rs @@ -1,6 +1,6 @@ mod tests { use paste::paste; - use ssh_rs::ssh; + use std::env; macro_rules! env_getter { diff --git a/tests/scp.rs b/tests/scp.rs index 344e7e9..0d6618e 100644 --- a/tests/scp.rs +++ b/tests/scp.rs @@ -1,7 +1,7 @@ +#[cfg(feature = "scp")] mod test { use paste::paste; - use ssh_rs::ssh; - use ssh_rs::LocalSession; + use ssh::{self, LocalSession}; use std::env; fn remove_file(filename: &str) { diff --git a/version b/version index 9fc80f9..0bfccb0 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.3.2 \ No newline at end of file +0.4.5 diff --git a/version-log b/version-log deleted file mode 100644 index af88b80..0000000 --- a/version-log +++ /dev/null @@ -1,47 +0,0 @@ -v0.3.1 (2023-01-10) - 1. hmac2使用异常问题修复 - 2. 新增aes-192-crt,aes-256-ctr加密算法 - -v0.3.1 (2022-12-07) - 部分问题修复 - -v0.3.0 (2022-11-18) - 1. 整体结构更新 - 2. 默认禁用ssh-rsa作为公钥加密算法,如需连接老版本服务器,需要启用dangerous-algorithms特性,并且自行add_pubkey_algorithms - -v0.2.2 (2022-11-05) - 1.除了默认的TcpStream之外,添加了一个新的 connect_bio API,要求用户提供其缓冲的输入/输出对象。 - 2.数据传输大于1GB密钥重新交换 - -v0.2.1 (2022-09-26) - 1.多次使用连接超时问题 - -v0.2.0 (2022-08-29) - 1.添加aes_ctr_128算法 - 2.添加hmac_sha1算法 - 3.默认设置为非阻塞 - 4.添加public_key认证方式 - -v0.1.5 (2022-06-13) - ChannelScp 修改为公用 - -v0.1.4 (2022-05-31) - 1.去除所有的互斥锁 - 2.window size问题修复 - 3.添加scp上传下载功能 - -v0.1.3 (2022-01-17): - 1.代码重构,修复不稳定代码 - 2.添加日志功能 - 3.可以直接通过 session 打开 exec shell - -v0.1.2 (2022-01-9): - 1.修复多线程使用shell交互失败的错误 - 2.修复 session 无法打开多个 channel 错误 - 3.去除 chrono 依赖 - -v0.1.1 (2022-01-5): - 修复不稳定的错误 - -v0.1.0 (2022-01-5): - 实现ssh协议通讯