diff --git a/Cargo.lock b/Cargo.lock index 9c44430..895f009 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,6 +431,7 @@ dependencies = [ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.116 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)", "simple_logger 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index f3258cb..2fa66c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ actix-rt = "1.1" dirs = "3.0" serde = "1.0" serde_yaml = "0.8" +serde_json = "1.0" handlebars = "3.5" hotwatch = "0.4" percent-encoding = "2.1" diff --git a/bunbun.default.yaml b/bunbun.default.yaml index c49d7cd..12f0c4d 100644 --- a/bunbun.default.yaml +++ b/bunbun.default.yaml @@ -17,11 +17,15 @@ default_route: "g" # defined route is used. # # You may provide an (absolute, recommended) path to an executable file to out- -# source route resolution to a program. The program will receive one argument -# only, which is the entire string provided from the user after matching the -# route. It is up to the out-sourced program to parse the arguments and to -# interpret those arguments. These programs should print one line to standard -# out, which should be a fully resolved URL to lead the user to. +# source route resolution to a program. The program will receive the arguments +# as space-separated words, without any shell parsing. +# +# These programs must return a JSON object with either one of the following +# key-value pairs: +# - "redirect": "some-path-to-redirect-to.com" +# - "body": The actual body to return. +# For example, to return a page that only prints out `3`, the function should +# return `{"redirect": "3"}`. # # These programs must be developed defensively, as they accept arbitrary user # input. Improper handling of user input can easily lead to anywhere from simple diff --git a/src/error.rs b/src/error.rs index 3c2dba3..48c51bd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,7 @@ pub enum BunBunError { InvalidConfigPath(std::path::PathBuf, std::io::Error), ConfigTooLarge(u64), ZeroByteConfig, + JsonParse(serde_json::Error), } impl Error for BunBunError {} @@ -29,7 +30,8 @@ impl fmt::Display for BunBunError { write!(f, "Failed to access {:?}: {}", path, reason) } Self::ConfigTooLarge(size) => write!(f, "The config file was too large ({} bytes)! Pass in --large-config to bypass this check.", size), - Self::ZeroByteConfig => write!(f, "The config provided reported a size of 0 bytes. Please check your config path!") + Self::ZeroByteConfig => write!(f, "The config provided reported a size of 0 bytes. Please check your config path!"), + Self::JsonParse(e) => e.fmt(f), } } } @@ -50,3 +52,4 @@ from_error!(std::io::Error, Io); from_error!(serde_yaml::Error, Parse); from_error!(hotwatch::Error, Watch); from_error!(log::SetLoggerError, LoggerInit); +from_error!(serde_json::Error, JsonParse); diff --git a/src/routes.rs b/src/routes.rs index 3ecb893..640a909 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -101,18 +101,18 @@ pub async fn hop( route_type: RouteType::External, path, .. - } => Ok(path.to_owned().into_bytes()), + } => Ok(HopAction::Redirect(path.clone())), }; match resolved_template { - Ok(path) => HttpResponse::Found() + Ok(HopAction::Redirect(path)) => HttpResponse::Found() .header( header::LOCATION, req .app_data::() .unwrap() .render_template( - std::str::from_utf8(&path).unwrap(), + std::str::from_utf8(path.as_bytes()).unwrap(), &template_args::query( utf8_percent_encode(&args, FRAGMENT_ENCODE_SET).to_string(), ), @@ -120,6 +120,7 @@ pub async fn hop( .unwrap(), ) .finish(), + Ok(HopAction::Body(body)) => HttpResponse::Ok().body(body), Err(e) => { error!("Failed to redirect user for {}: {}", path, e); HttpResponse::InternalServerError().body("Something went wrong :(\n") @@ -203,15 +204,24 @@ fn check_route(route: &Route, arg_count: usize) -> bool { true } +#[derive(Deserialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum HopAction { + Redirect(String), + Body(String), +} + /// Runs the executable with the user's input as a single argument. Returns Ok /// so long as the executable was successfully executed. Returns an Error if the /// file doesn't exist or bunbun did not have permission to read and execute the /// file. -fn resolve_path(path: PathBuf, args: &str) -> Result, BunBunError> { - let output = Command::new(path.canonicalize()?).arg(args).output()?; +fn resolve_path(path: PathBuf, args: &str) -> Result { + let output = Command::new(path.canonicalize()?) + .args(args.split(" ")) + .output()?; if output.status.success() { - Ok(output.stdout) + Ok(serde_json::from_slice(&output.stdout[..])?) } else { error!( "Program exit code for {} was not 0! Dumping standard error!", @@ -359,7 +369,7 @@ mod check_route { #[cfg(test)] mod resolve_path { - use super::resolve_path; + use super::{resolve_path, HopAction}; use std::env::current_dir; use std::path::PathBuf; @@ -370,7 +380,9 @@ mod resolve_path { #[test] fn valid_path_returns_ok() { - assert!(resolve_path(PathBuf::from("/bin/echo"), "hello").is_ok()); + assert!( + resolve_path(PathBuf::from("/bin/echo"), r#"{"body": "a"}"#).is_ok() + ); } #[test] @@ -379,7 +391,7 @@ mod resolve_path { let nest_level = current_dir().unwrap().ancestors().count() - 1; let mut rel_path = PathBuf::from("../".repeat(nest_level)); rel_path.push("./bin/echo"); - assert!(resolve_path(rel_path, "hello").is_ok()); + assert!(resolve_path(rel_path, r#"{"body": "a"}"#).is_ok()); } #[test] @@ -399,4 +411,20 @@ mod resolve_path { // cat-ing a folder always returns exit code 1 assert!(resolve_path(PathBuf::from("/bin/cat"), "/").is_err()); } + + #[test] + fn return_body() { + assert_eq!( + resolve_path(PathBuf::from("/bin/echo"), r#"{"body": "a"}"#).unwrap(), + HopAction::Body("a".to_string()) + ); + } + + #[test] + fn return_redirect() { + assert_eq!( + resolve_path(PathBuf::from("/bin/echo"), r#"{"redirect": "a"}"#).unwrap(), + HopAction::Redirect("a".to_string()) + ); + } }