Add JSON parsing from executables

This commit is contained in:
Edward Shen 2020-09-28 00:51:02 -04:00
parent b1cdce7c85
commit ce21a63f16
Signed by: edward
GPG key ID: 19182661E818369F
5 changed files with 52 additions and 15 deletions

1
Cargo.lock generated
View file

@ -431,6 +431,7 @@ dependencies = [
"log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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 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)", "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)", "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)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -15,6 +15,7 @@ actix-rt = "1.1"
dirs = "3.0" dirs = "3.0"
serde = "1.0" serde = "1.0"
serde_yaml = "0.8" serde_yaml = "0.8"
serde_json = "1.0"
handlebars = "3.5" handlebars = "3.5"
hotwatch = "0.4" hotwatch = "0.4"
percent-encoding = "2.1" percent-encoding = "2.1"

View file

@ -17,11 +17,15 @@ default_route: "g"
# defined route is used. # defined route is used.
# #
# You may provide an (absolute, recommended) path to an executable file to out- # 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 # source route resolution to a program. The program will receive the arguments
# only, which is the entire string provided from the user after matching the # as space-separated words, without any shell parsing.
# 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 # These programs must return a JSON object with either one of the following
# out, which should be a fully resolved URL to lead the user to. # 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 # These programs must be developed defensively, as they accept arbitrary user
# input. Improper handling of user input can easily lead to anywhere from simple # input. Improper handling of user input can easily lead to anywhere from simple

View file

@ -12,6 +12,7 @@ pub enum BunBunError {
InvalidConfigPath(std::path::PathBuf, std::io::Error), InvalidConfigPath(std::path::PathBuf, std::io::Error),
ConfigTooLarge(u64), ConfigTooLarge(u64),
ZeroByteConfig, ZeroByteConfig,
JsonParse(serde_json::Error),
} }
impl Error for BunBunError {} impl Error for BunBunError {}
@ -29,7 +30,8 @@ impl fmt::Display for BunBunError {
write!(f, "Failed to access {:?}: {}", path, reason) 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::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!(serde_yaml::Error, Parse);
from_error!(hotwatch::Error, Watch); from_error!(hotwatch::Error, Watch);
from_error!(log::SetLoggerError, LoggerInit); from_error!(log::SetLoggerError, LoggerInit);
from_error!(serde_json::Error, JsonParse);

View file

@ -101,18 +101,18 @@ pub async fn hop(
route_type: RouteType::External, route_type: RouteType::External,
path, path,
.. ..
} => Ok(path.to_owned().into_bytes()), } => Ok(HopAction::Redirect(path.clone())),
}; };
match resolved_template { match resolved_template {
Ok(path) => HttpResponse::Found() Ok(HopAction::Redirect(path)) => HttpResponse::Found()
.header( .header(
header::LOCATION, header::LOCATION,
req req
.app_data::<Handlebars>() .app_data::<Handlebars>()
.unwrap() .unwrap()
.render_template( .render_template(
std::str::from_utf8(&path).unwrap(), std::str::from_utf8(path.as_bytes()).unwrap(),
&template_args::query( &template_args::query(
utf8_percent_encode(&args, FRAGMENT_ENCODE_SET).to_string(), utf8_percent_encode(&args, FRAGMENT_ENCODE_SET).to_string(),
), ),
@ -120,6 +120,7 @@ pub async fn hop(
.unwrap(), .unwrap(),
) )
.finish(), .finish(),
Ok(HopAction::Body(body)) => HttpResponse::Ok().body(body),
Err(e) => { Err(e) => {
error!("Failed to redirect user for {}: {}", path, e); error!("Failed to redirect user for {}: {}", path, e);
HttpResponse::InternalServerError().body("Something went wrong :(\n") HttpResponse::InternalServerError().body("Something went wrong :(\n")
@ -203,15 +204,24 @@ fn check_route(route: &Route, arg_count: usize) -> bool {
true 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 /// 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 /// 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 doesn't exist or bunbun did not have permission to read and execute the
/// file. /// file.
fn resolve_path(path: PathBuf, args: &str) -> Result<Vec<u8>, BunBunError> { fn resolve_path(path: PathBuf, args: &str) -> Result<HopAction, BunBunError> {
let output = Command::new(path.canonicalize()?).arg(args).output()?; let output = Command::new(path.canonicalize()?)
.args(args.split(" "))
.output()?;
if output.status.success() { if output.status.success() {
Ok(output.stdout) Ok(serde_json::from_slice(&output.stdout[..])?)
} else { } else {
error!( error!(
"Program exit code for {} was not 0! Dumping standard error!", "Program exit code for {} was not 0! Dumping standard error!",
@ -359,7 +369,7 @@ mod check_route {
#[cfg(test)] #[cfg(test)]
mod resolve_path { mod resolve_path {
use super::resolve_path; use super::{resolve_path, HopAction};
use std::env::current_dir; use std::env::current_dir;
use std::path::PathBuf; use std::path::PathBuf;
@ -370,7 +380,9 @@ mod resolve_path {
#[test] #[test]
fn valid_path_returns_ok() { 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] #[test]
@ -379,7 +391,7 @@ mod resolve_path {
let nest_level = current_dir().unwrap().ancestors().count() - 1; let nest_level = current_dir().unwrap().ancestors().count() - 1;
let mut rel_path = PathBuf::from("../".repeat(nest_level)); let mut rel_path = PathBuf::from("../".repeat(nest_level));
rel_path.push("./bin/echo"); rel_path.push("./bin/echo");
assert!(resolve_path(rel_path, "hello").is_ok()); assert!(resolve_path(rel_path, r#"{"body": "a"}"#).is_ok());
} }
#[test] #[test]
@ -399,4 +411,20 @@ mod resolve_path {
// cat-ing a folder always returns exit code 1 // cat-ing a folder always returns exit code 1
assert!(resolve_path(PathBuf::from("/bin/cat"), "/").is_err()); 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())
);
}
} }