Add JSON parsing from executables
This commit is contained in:
parent
b1cdce7c85
commit
ce21a63f16
5 changed files with 52 additions and 15 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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)",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue