bunbun/src/routes.rs

431 lines
11 KiB
Rust
Raw Normal View History

use crate::config::{Route as ConfigRoute, RouteType};
2020-01-01 04:07:01 +00:00
use crate::{template_args, BunBunError, Route, State};
2019-12-23 15:02:21 +00:00
use actix_web::web::{Data, Query};
2020-01-01 01:26:06 +00:00
use actix_web::{get, http::header};
use actix_web::{HttpRequest, HttpResponse, Responder};
use handlebars::Handlebars;
2020-01-01 00:12:17 +00:00
use log::{debug, error};
2019-12-23 15:02:21 +00:00
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
2020-01-01 04:07:01 +00:00
use serde::Deserialize;
2019-12-23 15:02:21 +00:00
use std::collections::HashMap;
2020-01-01 00:23:51 +00:00
use std::path::PathBuf;
2020-01-01 00:12:17 +00:00
use std::process::Command;
2019-12-23 15:02:21 +00:00
use std::sync::{Arc, RwLock};
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
2019-12-24 03:21:42 +00:00
const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
2019-12-23 15:20:35 +00:00
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`')
2020-09-27 20:09:46 +00:00
.add(b'+')
.add(b'&') // Interpreted as a GET query
.add(b'#'); // Interpreted as a hyperlink section target
2019-12-23 15:02:21 +00:00
2019-12-31 22:36:21 +00:00
type StateData = Data<Arc<RwLock<State>>>;
2020-01-01 04:00:54 +00:00
#[get("/")]
pub async fn index(data: StateData, req: HttpRequest) -> impl Responder {
let data = data.read().unwrap();
HttpResponse::Ok()
.set_header(header::CONTENT_TYPE, "text/html; charset=utf-8")
.body(
req
.app_data::<Handlebars>()
.unwrap()
.render(
"index",
&template_args::hostname(data.public_address.clone()),
)
.unwrap(),
)
2020-01-01 04:00:54 +00:00
}
#[get("/bunbunsearch.xml")]
pub async fn opensearch(data: StateData, req: HttpRequest) -> impl Responder {
let data = data.read().unwrap();
HttpResponse::Ok()
.header(
header::CONTENT_TYPE,
"application/opensearchdescription+xml",
)
.body(
req
.app_data::<Handlebars>()
.unwrap()
.render(
"opensearch",
&template_args::hostname(data.public_address.clone()),
)
.unwrap(),
)
}
2019-12-23 15:02:21 +00:00
#[get("/ls")]
2020-01-01 04:00:54 +00:00
pub async fn list(data: StateData, req: HttpRequest) -> impl Responder {
2019-12-23 15:02:21 +00:00
let data = data.read().unwrap();
HttpResponse::Ok()
.set_header(header::CONTENT_TYPE, "text/html; charset=utf-8")
.body(
req
.app_data::<Handlebars>()
.unwrap()
.render("list", &data.groups)
.unwrap(),
)
2019-12-23 15:02:21 +00:00
}
#[derive(Deserialize)]
pub struct SearchQuery {
to: String,
}
#[get("/hop")]
2019-12-26 20:06:00 +00:00
pub async fn hop(
2019-12-26 21:18:15 +00:00
data: StateData,
req: HttpRequest,
2019-12-23 15:02:21 +00:00
query: Query<SearchQuery>,
) -> impl Responder {
let data = data.read().unwrap();
match resolve_hop(&query.to, &data.routes, &data.default_route) {
2020-09-27 21:02:43 +00:00
RouteResolution::Resolved { route: path, args } => {
2020-01-01 00:12:17 +00:00
let resolved_template = match path {
ConfigRoute {
route_type: RouteType::Internal,
path,
..
} => resolve_path(PathBuf::from(path), &args),
ConfigRoute {
route_type: RouteType::External,
path,
..
2020-09-28 04:51:02 +00:00
} => Ok(HopAction::Redirect(path.clone())),
2020-01-01 00:12:17 +00:00
};
match resolved_template {
2020-09-28 04:51:02 +00:00
Ok(HopAction::Redirect(path)) => HttpResponse::Found()
2020-01-01 00:12:17 +00:00
.header(
header::LOCATION,
req
.app_data::<Handlebars>()
.unwrap()
.render_template(
2020-09-28 04:51:02 +00:00
std::str::from_utf8(path.as_bytes()).unwrap(),
2020-01-01 00:12:17 +00:00
&template_args::query(
utf8_percent_encode(&args, FRAGMENT_ENCODE_SET).to_string(),
),
)
.unwrap(),
2019-12-23 15:02:21 +00:00
)
2020-01-01 00:12:17 +00:00
.finish(),
2020-09-28 04:51:02 +00:00
Ok(HopAction::Body(body)) => HttpResponse::Ok().body(body),
2020-01-01 00:12:17 +00:00
Err(e) => {
2020-01-01 00:42:53 +00:00
error!("Failed to redirect user for {}: {}", path, e);
HttpResponse::InternalServerError().body("Something went wrong :(\n")
2020-01-01 00:12:17 +00:00
}
}
}
2020-09-27 21:02:43 +00:00
RouteResolution::Unresolved => HttpResponse::NotFound().body("not found"),
2019-12-23 15:02:21 +00:00
}
}
2020-09-27 21:02:43 +00:00
#[derive(Debug, PartialEq)]
enum RouteResolution<'a> {
Resolved { route: &'a Route, args: String },
Unresolved,
}
2019-12-23 15:02:21 +00:00
/// Attempts to resolve the provided string into its route and its arguments.
/// If a default route was provided, then this will consider that route before
/// failing to resolve a route.
///
/// The first element in the tuple describes the route, while the second element
/// returns the remaining arguments. If none remain, an empty string is given.
2019-12-31 22:36:21 +00:00
fn resolve_hop<'a>(
2019-12-23 15:02:21 +00:00
query: &str,
2019-12-31 22:36:21 +00:00
routes: &'a HashMap<String, Route>,
2019-12-23 15:02:21 +00:00
default_route: &Option<String>,
2020-09-27 21:02:43 +00:00
) -> RouteResolution<'a> {
2019-12-23 15:02:21 +00:00
let mut split_args = query.split_ascii_whitespace().peekable();
2020-09-27 23:37:24 +00:00
let maybe_route = {
match split_args.peek() {
Some(command) => routes.get(*command),
None => {
debug!("Found empty query, returning no route.");
return RouteResolution::Unresolved;
}
2019-12-23 19:09:49 +00:00
}
2019-12-23 15:02:21 +00:00
};
2020-09-28 01:33:32 +00:00
let args = split_args.collect::<Vec<_>>();
let arg_count = args.len();
2020-09-27 23:37:24 +00:00
// Try resolving with a matched command
if let Some(route) = maybe_route {
2020-09-28 01:33:32 +00:00
let args = if args.is_empty() { &[] } else { &args[1..] }.join(" ");
let arg_count = arg_count - 1;
if check_route(route, arg_count) {
debug!("Resolved {} with args {}", route, args);
return RouteResolution::Resolved { route, args };
}
2020-09-27 23:37:24 +00:00
}
// Try resolving with the default route, if it exists
if let Some(route) = default_route {
if let Some(route) = routes.get(route) {
2020-09-28 01:33:32 +00:00
if check_route(route, arg_count) {
let args = args.join(" ");
debug!("Using default route {} with args {}", route, args);
return RouteResolution::Resolved { route, args };
}
2019-12-23 19:09:49 +00:00
}
2019-12-23 15:02:21 +00:00
}
2020-09-27 23:37:24 +00:00
RouteResolution::Unresolved
2019-12-23 15:02:21 +00:00
}
2020-09-28 01:33:32 +00:00
/// Checks if the user provided string has the correct properties required by
/// the route to be successfully matched.
fn check_route(route: &Route, arg_count: usize) -> bool {
if let Some(min_args) = route.min_args {
if arg_count < min_args {
return false;
}
}
if let Some(max_args) = route.max_args {
if arg_count > max_args {
return false;
}
}
true
}
2020-09-28 04:51:02 +00:00
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum HopAction {
Redirect(String),
Body(String),
}
2020-01-01 00:42:53 +00:00
/// 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
2020-01-01 01:04:51 +00:00
/// file.
2020-09-28 04:51:02 +00:00
fn resolve_path(path: PathBuf, args: &str) -> Result<HopAction, BunBunError> {
let output = Command::new(path.canonicalize()?)
.args(args.split(" "))
.output()?;
2020-01-01 00:42:53 +00:00
if output.status.success() {
2020-09-28 04:51:02 +00:00
Ok(serde_json::from_slice(&output.stdout[..])?)
2020-01-01 00:42:53 +00:00
} else {
error!(
"Program exit code for {} was not 0! Dumping standard error!",
path.display(),
);
let error = String::from_utf8_lossy(&output.stderr);
2020-07-05 00:44:30 +00:00
Err(BunBunError::CustomProgram(error.to_string()))
2020-01-01 00:42:53 +00:00
}
2020-01-01 00:12:17 +00:00
}
2019-12-29 05:08:13 +00:00
#[cfg(test)]
mod resolve_hop {
use super::*;
2020-07-05 03:43:06 +00:00
use std::str::FromStr;
2019-12-29 05:08:13 +00:00
2019-12-31 22:36:21 +00:00
fn generate_route_result<'a>(
keyword: &'a Route,
2019-12-29 05:08:13 +00:00
args: &str,
2020-09-27 21:02:43 +00:00
) -> RouteResolution<'a> {
RouteResolution::Resolved {
route: keyword,
args: String::from(args),
}
2019-12-29 05:08:13 +00:00
}
#[test]
fn empty_routes_no_default_yields_failed_hop() {
assert_eq!(
resolve_hop("hello world", &HashMap::new(), &None),
2020-09-27 21:02:43 +00:00
RouteResolution::Unresolved
2019-12-29 05:08:13 +00:00
);
}
#[test]
fn empty_routes_some_default_yields_failed_hop() {
assert_eq!(
resolve_hop(
"hello world",
&HashMap::new(),
&Some(String::from("google"))
),
2020-09-27 21:02:43 +00:00
RouteResolution::Unresolved
2019-12-29 05:08:13 +00:00
);
}
#[test]
fn only_default_routes_some_default_yields_default_hop() {
2019-12-31 22:36:21 +00:00
let mut map: HashMap<String, Route> = HashMap::new();
map.insert(
"google".into(),
2020-07-05 03:43:06 +00:00
Route::from_str("https://example.com").unwrap(),
2019-12-31 22:36:21 +00:00
);
2019-12-29 05:08:13 +00:00
assert_eq!(
resolve_hop("hello world", &map, &Some(String::from("google"))),
2019-12-31 22:36:21 +00:00
generate_route_result(
2020-07-05 03:43:06 +00:00
&Route::from_str("https://example.com").unwrap(),
2019-12-31 22:36:21 +00:00
"hello world"
),
2019-12-29 05:08:13 +00:00
);
}
#[test]
fn non_default_routes_some_default_yields_non_default_hop() {
2019-12-31 22:36:21 +00:00
let mut map: HashMap<String, Route> = HashMap::new();
map.insert(
"google".into(),
2020-07-05 03:43:06 +00:00
Route::from_str("https://example.com").unwrap(),
2019-12-31 22:36:21 +00:00
);
2019-12-29 05:08:13 +00:00
assert_eq!(
resolve_hop("google hello world", &map, &Some(String::from("a"))),
2019-12-31 22:36:21 +00:00
generate_route_result(
2020-07-05 03:43:06 +00:00
&Route::from_str("https://example.com").unwrap(),
2019-12-31 22:36:21 +00:00
"hello world"
),
2019-12-29 05:08:13 +00:00
);
}
#[test]
fn non_default_routes_no_default_yields_non_default_hop() {
2019-12-31 22:36:21 +00:00
let mut map: HashMap<String, Route> = HashMap::new();
map.insert(
"google".into(),
2020-07-05 03:43:06 +00:00
Route::from_str("https://example.com").unwrap(),
2019-12-31 22:36:21 +00:00
);
2019-12-29 05:08:13 +00:00
assert_eq!(
resolve_hop("google hello world", &map, &None),
2019-12-31 22:36:21 +00:00
generate_route_result(
2020-07-05 03:43:06 +00:00
&Route::from_str("https://example.com").unwrap(),
2019-12-31 22:36:21 +00:00
"hello world"
),
2019-12-29 05:08:13 +00:00
);
}
}
2020-01-01 05:08:47 +00:00
2020-09-28 01:33:32 +00:00
#[cfg(test)]
mod check_route {
use super::*;
fn create_route(
min_args: impl Into<Option<usize>>,
max_args: impl Into<Option<usize>>,
) -> Route {
Route {
description: None,
hidden: false,
max_args: max_args.into(),
min_args: min_args.into(),
path: String::new(),
route_type: RouteType::External,
}
}
#[test]
fn no_min_arg_no_max_arg_counts() {
assert!(check_route(&create_route(None, None), 0));
assert!(check_route(&create_route(None, None), usize::MAX));
}
#[test]
fn min_arg_no_max_arg_counts() {
assert!(!check_route(&create_route(3, None), 0));
assert!(!check_route(&create_route(3, None), 2));
assert!(check_route(&create_route(3, None), 3));
assert!(check_route(&create_route(3, None), 4));
assert!(check_route(&create_route(3, None), usize::MAX));
}
#[test]
fn no_min_arg_max_arg_counts() {
assert!(check_route(&create_route(None, 3), 0));
assert!(check_route(&create_route(None, 3), 2));
assert!(check_route(&create_route(None, 3), 3));
assert!(!check_route(&create_route(None, 3), 4));
assert!(!check_route(&create_route(None, 3), usize::MAX));
}
#[test]
fn min_arg_max_arg_counts() {
assert!(!check_route(&create_route(2, 3), 1));
assert!(check_route(&create_route(2, 3), 2));
assert!(check_route(&create_route(2, 3), 3));
assert!(!check_route(&create_route(2, 3), 4));
}
}
2020-01-01 05:08:47 +00:00
#[cfg(test)]
mod resolve_path {
2020-09-28 04:51:02 +00:00
use super::{resolve_path, HopAction};
2020-01-01 05:08:47 +00:00
use std::env::current_dir;
use std::path::PathBuf;
#[test]
fn invalid_path_returns_err() {
assert!(resolve_path(PathBuf::from("/bin/aaaa"), "aaaa").is_err());
}
#[test]
fn valid_path_returns_ok() {
2020-09-28 04:51:02 +00:00
assert!(
resolve_path(PathBuf::from("/bin/echo"), r#"{"body": "a"}"#).is_ok()
);
2020-01-01 05:08:47 +00:00
}
#[test]
fn relative_path_returns_ok() {
// How many ".." needed to get to /
let nest_level = current_dir().unwrap().ancestors().count() - 1;
let mut rel_path = PathBuf::from("../".repeat(nest_level));
rel_path.push("./bin/echo");
2020-09-28 04:51:02 +00:00
assert!(resolve_path(rel_path, r#"{"body": "a"}"#).is_ok());
2020-01-01 05:08:47 +00:00
}
#[test]
fn no_permissions_returns_err() {
assert!(
// Trying to run a command without permission
format!(
"{}",
resolve_path(PathBuf::from("/root/some_exec"), "").unwrap_err()
)
.contains("Permission denied")
);
}
#[test]
fn non_success_exit_code_yields_err() {
// cat-ing a folder always returns exit code 1
assert!(resolve_path(PathBuf::from("/bin/cat"), "/").is_err());
}
2020-09-28 04:51:02 +00:00
#[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())
);
}
2020-01-01 05:08:47 +00:00
}