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)",
"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)",

View file

@ -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"

View file

@ -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

View file

@ -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);

View file

@ -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::<Handlebars>()
.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<Vec<u8>, BunBunError> {
let output = Command::new(path.canonicalize()?).arg(args).output()?;
fn resolve_path(path: PathBuf, args: &str) -> Result<HopAction, BunBunError> {
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())
);
}
}