diff --git a/.gitignore b/.gitignore index 0975025..ffbb106 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ target/ # Ignore vim temp and swap files *~ *.sw? + +# npm +test/bats +test/test_helper diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b7efcb4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "test/bats"] + path = test/bats + url = https://github.com/bats-core/bats-core.git +[submodule "test/test_helper/bats-support"] + path = test/test_helper/bats-support + url = https://github.com/bats-core/bats-support.git +[submodule "test/test_helper/bats-assert"] + path = test/test_helper/bats-assert + url = https://github.com/bats-core/bats-assert.git diff --git a/Cargo.lock b/Cargo.lock index aade198..747cb3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,122 +3,161 @@ version = 3 [[package]] -name = "atty" -version = "0.2.14" +name = "anstream" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", ] [[package]] -name = "autocfg" -version = "1.1.0" +name = "anstyle" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] -name = "bitflags" -version = "1.3.2" +name = "anstyle-parse" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] [[package]] -name = "clap" -version = "3.2.14" +name = "anstyle-query" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54635806b078b7925d6e36810b1755f2a4b5b4d57560432c1ecf60bcbe10602b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "atty", - "bitflags", - "clap_lex", - "indexmap", - "once_cell", - "strsim", - "termcolor", - "textwrap", + "windows-sys", ] [[package]] -name = "clap_lex" -version = "0.2.4" +name = "anstyle-wincon" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ - "os_str_bytes", + "anstyle", + "windows-sys", ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cfg-if" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "clap" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ - "libc", + "clap_builder", ] [[package]] -name = "indexmap" -version = "1.9.1" +name = "clap_builder" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ - "autocfg", - "hashbrown", + "anstream", + "anstyle", + "clap_lex", + "strsim", ] [[package]] -name = "libc" -version = "0.2.126" +name = "clap_lex" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] -name = "once_cell" -version = "1.13.0" +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "js-sys" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] [[package]] -name = "os_str_bytes" -version = "6.2.0" +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "once_cell" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + [[package]] name = "resh" -version = "0.1.4" +version = "0.2.0" dependencies = [ "clap", "serde", "serde_derive", "toml", + "whoami", ] [[package]] @@ -135,14 +174,14 @@ checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" @@ -156,20 +195,16 @@ dependencies = [ ] [[package]] -name = "termcolor" -version = "1.1.3" +name = "syn" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ - "winapi-util", + "proc-macro2", + "quote", + "unicode-ident", ] -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - [[package]] name = "toml" version = "0.5.9" @@ -186,32 +221,161 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] -name = "winapi" -version = "0.3.9" +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "cfg-if", + "wasm-bindgen-macro", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "wasm-bindgen-backend" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "winapi-util" -version = "0.1.5" +name = "whoami" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "winapi", + "redox_syscall", + "wasite", + "web-sys", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml index ba9e204..cd95c9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "resh" -version = "0.1.5" +version = "0.2.0" authors = ["Koen Wilde "] description = "A restricted shell that only allows execution of previously defined aliases" edition = "2018" @@ -16,4 +16,5 @@ keywords = ["shell", "ssh", "restricted", "nrpe"] serde = "1.0" serde_derive = "1.0" toml = "0.5" -clap = { version = "3", features = ["cargo"] } +clap = { version = "4", features = ["cargo"] } +whoami = "1.5.1" \ No newline at end of file diff --git a/README.md b/README.md index 6e19093..f89cdb9 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ # resh -[![Build Status](https://api.travis-ci.org/koenw/resh.svg?branch=master)](https://travis-ci.org/koenw/resh) - -`resh` is a shell that only allows the execution of previously -whitelisted commands. Use it to restrict automated ssh logins. +`resh` is a secure restricted production-grade ssh-compatible shell built in rust ## Usage -Define aliases for the commands you want to allow in the *commands* section of +Define aliases for the commands you want to allow in the _commands_ section of `/etc/resh.toml`: ```sh $ cat /etc/resh.toml [commands] foo = "echo hello" +[user_commands.john] +echo = "echo %@" +foo = "echo hello override" ``` Next set resh as the login shell for the user you want to restrict. The user @@ -28,7 +28,18 @@ Undefined command alias: bar hello ``` -Or using ssh: +Or interactive mode: + +```sh +# su - example_user +resh > +# foo +hello +resh > +``` + +using ssh: + ```sh $ ssh example_user@localhost foo hello @@ -54,7 +65,7 @@ $ cat ~example_user/.ssh/authorized_keys command="/usr/local/bin/resh" AAAAB3NzaC1yc2EAAAADAQABAAACAQD7BsnSaa0gkPJDGZM7psAEkx+68ILJlKHS6MlUfVpQu7UoercvJXqctHczeIEf1eJToK7RmiKufoicLkHQplRpI9kP4IDAx2V0LO4BRncIOyF8wk6I7N6k6glAxePA4MgPaSsFp8SyXYW9wy+0491YHr9sWaqaKG78OQSCyf+/wwynRnwdn2u0dcRl064CGxrYleGe0AHHOSl9jj9J2Ve6M7pjZLuixRLqB2VBYyIAwy/zO7dvuxxvLIGr31TqKdLnnUvLKeInn5IU+UPMxuHG9DC9yLnif29OUzNRERTF4utkRI+ywByFTj/QePp+uTvmVv0PtkGwm77LKxeBP7jP3Hhe2uvf5clApcF+6EjFBNKWxVReH35NGPasY8DNL7Mt5CfBZcdi4nhQZyCQ7Z/XlXmJRMxmYsowhHQB8HkOM8MpHPqP9EBf9eTnxhMaA5qnrSy/z+1vdKHVXc4camSF8z7dRJKDmuoYl+aPcjS5MX6AEVz5gtFsizjhLq+mp2HkvskSZCPY87D0/hriPPtSMUlhh4XKyFJ2VzkfIr1uqQlaN1tIPdCAdUDjH5o5fnqSFHqkD8iah8OiNhmGLk2VPiYohnMLcDdLGtPMkOpX3ODgjNOTcaUfaMZW4IacVcHA2A11Zxe8r73qcjKjcX5mEppMa1Z2vosqJn2dGTasHQ== example_user@example ``` -### Full ssh *`authorized_keys`* example +### Full ssh _`authorized_keys`_ example ```sh $ cat ~example_user/.ssh/authorized_keys @@ -62,7 +73,7 @@ command="/usr/local/bin/resh",environment="RESH_CONFIG=/usr/local/etc/resh.toml" ``` For more information on the options you can specify in the `authorized_keys` -file, refer to the *`AUTHORIZED_KEYS FILE FORMAT`* section of `man 8 sshd`. You +file, refer to the _`AUTHORIZED_KEYS FILE FORMAT`_ section of `man 8 sshd`. You may especially be interested in the `restrict` option, which disables features like tcp port forwarding. @@ -86,9 +97,9 @@ your `$PATH` if cargo is installed correctly. ## Future ideas -* Possibly support an *IncludeDir* option in the config file, for easier +- Possibly support an _IncludeDir_ option in the config file, for easier provisioning from e.g. puppet or ansible. -* Provide pre-build binaries for OpenBSD, FreeBSD and linux. +- Provide pre-build binaries for OpenBSD, FreeBSD and linux. ## Feedback & Questions diff --git a/src/main.rs b/src/main.rs index 19e1938..9330d0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,19 @@ #[macro_use] extern crate serde_derive; -use std::collections::BTreeMap; -use std::fs::File; -use std::io::prelude::*; - -use clap::{App, Arg}; +use clap::{crate_authors, crate_name, crate_version, Arg, Command}; +use std::{ + collections::BTreeMap, + error::Error, + fs::File, + io::{self, prelude::*, BufRead, Write}, + process::Command as ProcessCommand, +}; +use whoami; macro_rules! die( ($($arg:tt)*) => { { - writeln!(std::io::stderr(), $($arg)*) - .expect("Failed to print to stderr"); + writeln!(io::stderr(), $($arg)*).expect("Failed to print to stderr"); std::process::exit(1); } } ); @@ -18,62 +21,109 @@ macro_rules! die( #[derive(Deserialize)] struct Config { commands: BTreeMap, + user_commands: Option>>, } -fn read_config>(path: P) -> Result> { +fn read_config>(path: P) -> Result> { let mut contents = String::new(); - File::open(path)?.read_to_string(&mut contents)?; - - let config: Config = toml::from_str(&contents)?; - Ok(config) + toml::from_str(&contents).map_err(|e| e.into()) } -fn run_command(command: &str) -> Result> { - let mut child = std::process::Command::new("/bin/sh") - .arg("-c") - .arg(command) - .spawn()?; +fn run_command(command: &str, args: &str) -> Result> { + let formatted_command = args + .split_whitespace() + .enumerate() + .fold(command.replace("%@", args), |cmd, (i, arg)| { + cmd.replace(&format!("%{}", i + 1), arg) + }); - child + ProcessCommand::new("/bin/sh") + .arg("-c") + .arg(&formatted_command) + .spawn()? .wait()? .code() - .ok_or_else(|| std::io::Error::last_os_error().into()) + .ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::Other, "No exit code available").into() + }) } fn main() { - let matches = App::new(clap::crate_name!()) - .version(clap::crate_version!()) - .author(clap::crate_authors!()) - .about("resh is a restricted (ssh) shell that only allows whitelisted commands") + let matches = Command::new(crate_name!()) + .version(crate_version!()) + .author(crate_authors!()) + .about("resh is a restricted shell that only allows whitelisted commands") .arg( - Arg::with_name("command") + Arg::new("command") .short('c') .help("Alias of command to execute") .value_name("COMMAND"), ) .get_matches(); - - let command_alias = match matches.value_of("command") { - Some(cmd) => String::from(cmd), - None => match std::env::var("SSH_ORIGINAL_COMMAND") { - Ok(cmd) => cmd, - _ => die!("Usage: {} ", clap::crate_name!()), - }, - }; - let config_file = std::env::var("RESH_CONFIG").unwrap_or_else(|_| "/etc/resh.toml".to_string()); + let config = + read_config(&config_file).unwrap_or_else(|e| die!("Failed to read {}: {}", config_file, e)); + + if let Some(command_alias_and_args) = std::env::var("SSH_ORIGINAL_COMMAND") + .ok() + .or_else(|| matches.get_one::("command").cloned()) + { + let mut command_args = command_alias_and_args.split_whitespace(); + execute_command(&config, &mut command_args); + } else { + interactive_mode(&config); + } +} - let config: Config = read_config(&config_file).unwrap_or_else(|e| { - die!("Failed to read {}: {}", config_file, e); - }); - - let full_command = match config.commands.get(&command_alias) { - Some(cmd) => cmd, - None => die!("Undefined command alias: {}", command_alias), +fn execute_command(config: &Config, command_args: &mut std::str::SplitWhitespace) -> i32 { + let command_alias = match command_args.next() { + Some(alias) => alias, + None => { + eprintln!("Usage: {} ", crate_name!()); + return 1; + } }; - let exitcode = run_command(full_command).unwrap_or(1); + let username = whoami::username(); + let command = config + .user_commands + .as_ref() + .and_then(|user_cmds| { + user_cmds + .get(&username) + .and_then(|cmds| cmds.get(command_alias)) + }) + .or_else(|| config.commands.get(command_alias)); + + match command { + Some(cmd) => match run_command(cmd, &command_args.collect::>().join(" ")) { + Ok(code) => code, + Err(e) => { + eprintln!("Error executing command: {}", e); + 1 + } + }, + None => { + eprintln!("Undefined command alias: {}", command_alias); + 1 + } + } +} - std::process::exit(exitcode); +fn interactive_mode(config: &Config) { + let stdin = io::stdin(); + let mut reader = stdin.lock(); + let mut line = String::new(); + + loop { + print!("resh> "); + io::stdout().flush().unwrap(); + line.clear(); + if reader.read_line(&mut line).unwrap() == 0 { + break; // EOF + } + let mut command_args = line.trim().split_whitespace(); + execute_command(config, &mut command_args); + } } diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..1d3dd16 --- /dev/null +++ b/test.sh @@ -0,0 +1 @@ +./test/bats/bin/bats test/test.bats \ No newline at end of file diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml new file mode 100644 index 0000000..8d422e0 --- /dev/null +++ b/test/docker-compose.yaml @@ -0,0 +1,74 @@ +services: + resh-test: + hostname: resh-test + container_name: resh-test + build: + context: ./.. + dockerfile_inline: | + FROM rust:alpine as builder + WORKDIR /usr/src + RUN apk add --no-cache musl-dev + + # The standard way to build + #COPY . . + #RUN cargo build --release + + # A workaround to avoid redownloading and recompling everything when only the source code changes + # This is in leui of a cargo build --deps-only command + COPY ./Cargo.toml ./Cargo.lock ./ + RUN mkdir src \ + && echo "fn main() {println!(\"if you see this, the build failed\")}" > src/main.rs \ + && cargo build + RUN rm src/main.rs + + # Copy the source code and build the release binary + COPY ./src ./src + RUN cargo build --release + + FROM alpine:latest + WORKDIR /root + COPY --from=builder /usr/src/target/release/resh . + + # SSH Server + RUN apk add --no-cache openssh-server + RUN sed -i 's/#Port 22/Port 1234/' /etc/ssh/sshd_config + RUN sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config + RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config + RUN sed -i 's/#ChallengeResponseAuthentication yes/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config + RUN sed -i 's/#KbdInteractiveAuthentication yes/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config + RUN echo 'AuthenticationMethods publickey' >> /etc/ssh/sshd_config + + # SSH Server + RUN ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N '' + # Root User Authoirzed Keys + RUN mkdir -p /root/.ssh/ + RUN cat /etc/ssh/ssh_host_ed25519_key.pub >> /root/.ssh/authorized_keys + RUN chmod 600 /root/.ssh/authorized_keys + # Start the SSHD server and keep it up + CMD ["/usr/sbin/sshd", "-D"] + volumes: + - resh-ssh-volume:/etc/ssh/ + networks: + - resh-isolated-network + + resh-test-ssh-client: + hostname: resh-test-ssh-client + container_name: resh-test-ssh-client + build: + context: . + dockerfile_inline: | + FROM alpine:latest + RUN apk add --no-cache openssh-client + RUN mkdir -p /root/.ssh + CMD tail -f /dev/null + volumes: + - resh-ssh-volume:/mnt/resh-ssh-volume/ + networks: + - resh-isolated-network + +networks: + resh-isolated-network: + driver: bridge + +volumes: + resh-ssh-volume: diff --git a/test/resh.toml b/test/resh.toml new file mode 100644 index 0000000..ab7384b --- /dev/null +++ b/test/resh.toml @@ -0,0 +1,7 @@ +[commands] +ls = "ls -l" +foo = 'echo bar' +[user_commands.root] +lsa = "ls -lah" +echo = "echo %@" +foo = 'echo bar override' \ No newline at end of file diff --git a/test/test.bats b/test/test.bats new file mode 100755 index 0000000..bbf7231 --- /dev/null +++ b/test/test.bats @@ -0,0 +1,88 @@ +#!/usr/bin/env bats + +setup() { + load '../test/test_helper/bats-support/load' + load '../test/test_helper/bats-assert/load' +} + +@test "Setup" { + run docker-compose -f test/docker-compose.yaml up --build -d + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client cp /mnt/resh-ssh-volume/ssh_host_ed25519_key /root/.ssh/ssh_host_ed25519_key + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client chmod 700 /root/.ssh + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client sh -c 'echo [resh-test]:1234 $(cat /mnt/resh-ssh-volume/ssh_host_ed25519_key.pub) >> /root/.ssh/known_hosts' + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client chmod 700 /root/.ssh/known_hosts + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client cat /root/.ssh/known_hosts + [ $status -eq 0 ] + + # Docker resh-test-ssh-client is authorized into resh-test + run docker exec -it resh-test-ssh-client ssh -i /root/.ssh/ssh_host_ed25519_key root@resh-test -p 1234 echo 'hello world!' + [ $status -eq 0 ] + refute_output --partial 'Connection refused' + refute_output --partial 'Permission denied' + assert_output --partial 'hello world!' +} + +@test "resh-test-toml-found" { + run docker cp test/resh.toml resh-test:/etc/resh.toml + [ $status -eq 0 ] + run docker exec -it resh-test ./resh -c + [ $status -ne 0 ] + assert_output --partial 'a value is required for' +} + +@test "resh-test-toml-global-command-valid" { + run docker exec -it resh-test ./resh -c ls + assert_output --partial 'root' +} + +@test "resh-test-toml-global-command-unavailable" { + run docker exec -it resh-test ./resh -c touch + assert_output --partial 'Undefined command alias' +} + +@test "resh-test-toml-global-user-command" { + run docker exec -it resh-test ./resh -c lsa + assert_output --partial '..' +} + +@test "resh-test-toml-global-user-command-with-args" { + run docker exec -it resh-test ./resh -c 'echo hello world!' + assert_output --partial 'hello world!' +} + +@test "resh-test-toml-global-user-command-overrides-global" { + run docker exec -it resh-test ./resh -c 'foo' + assert_output --partial 'bar override' +} + +@test "resh-test-ssh-entrypoint-full" { + run docker exec -it resh-test mkdir -p /usr/local/etc + [ $status -eq 0 ] + run docker cp test/resh.toml resh-test:/usr/local/etc/resh.toml + [ $status -eq 0 ] + run docker exec resh-test /bin/sh -c "sed -i 's/^/command=\"\\/root\\/resh\",environment=\"RESH_CONFIG=\\/usr\\/local\\/etc\\/resh.toml\", /' /root/.ssh/authorized_keys" + [ $status -eq 0 ] + run docker exec -it resh-test-ssh-client ssh -i /root/.ssh/ssh_host_ed25519_key root@resh-test -p 1234 foo + [ $status -eq 0 ] + refute_output --partial 'Connection refused' + refute_output --partial 'Permission denied' + assert_output --partial 'bar override' +} + +@test "Teardown" { + teardown_once +} + +teardown_once() { + docker kill resh-test + docker kill resh-test-ssh-client + docker rm resh-test + docker rm resh-test-ssh-client + docker volume rm test_resh-ssh-volume + docker network rm test_resh-isolated-network +}