From f79b75710167088e2031a82a94a8d127fbb16a1f Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Fri, 12 Nov 2021 22:38:39 -0700 Subject: god is dead and we have killed him. --- Cargo.lock | 126 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +- src/main.rs | 122 +++++++++++++++++++++++++++++++++- src/utils.rs | 2 + src/utils/proxy_child.rs | 74 +++++++++++++++++++++ src/utils/serve_static.rs | 75 +++++++++++++++++++++ tests/config-example/main.rs | 16 +++-- tests/config-example/narchttpd.rhai | 21 +++--- tests/lib.rs | 19 ++++++ 9 files changed, 442 insertions(+), 16 deletions(-) create mode 100644 src/utils.rs create mode 100644 src/utils/proxy_child.rs create mode 100644 src/utils/serve_static.rs create mode 100644 tests/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ad40ff6..78bbea7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,26 @@ dependencies = [ "version_check", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -37,6 +57,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "fnv" version = "1.0.7" @@ -119,6 +154,15 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -275,6 +319,7 @@ version = "0.1.0" dependencies = [ "hyper", "rhai", + "structopt", "tokio", ] @@ -349,6 +394,30 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.32" @@ -454,6 +523,36 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.81" @@ -465,6 +564,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "tokio" version = "1.13.0" @@ -542,12 +650,30 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.3" diff --git a/Cargo.toml b/Cargo.toml index ac13e55..44cd718 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" [dependencies] hyper = { version = "0.14", features = ["full"] } -rhai = { version = "1.1", features = ["unicode-xid-ident", "no_closure"] } +rhai = { version = "1.1", features = ["unicode-xid-ident", "no_closure", "internals"] } +structopt = "0.3.25" tokio = { version = "1", features = ["full"] } diff --git a/src/main.rs b/src/main.rs index 97a8ae6..0951518 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,124 @@ +use std::convert::Infallible; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; + +use hyper::service::{make_service_fn, service_fn}; +use hyper::{header, Body, Request, Response, Server, StatusCode}; +use rhai::{Dynamic, Engine, FnPtr, Map, NativeCallContext, Scope}; +use structopt::StructOpt; + +mod utils; + +#[derive(Debug, StructOpt)] +struct Opt { + #[structopt(long, parse(from_os_str), default_value = "narchttpd.rhai")] + config_script: PathBuf, +} + +fn make_engine() -> Engine { + let mut engine = Engine::new(); + engine.register_type_with_name::>>("Request"); + engine.register_type::(); + engine.register_fn( + "handle_request_serve_static", + utils::serve_static::handle_request, + ); + engine.register_fn("serve_static", utils::serve_static::serve_static); + engine.register_fn( + "handle_request_proxy_child", + utils::proxy_child::handle_request, + ); + engine.register_fn("proxy_child", utils::proxy_child::proxy_child); + engine +} + +fn get_config_scope(engine: &Engine, opt: &Opt) -> Scope<'static> { + let mut ast = engine.compile_file(opt.config_script.clone()).unwrap(); + + let mut scope = Scope::new(); + scope.push("http_ports", [80]); + scope.push("https_ports", [443]); + scope.push("domains", Map::new()); + let export_ast = engine + .compile("export http_ports, https_ports, domains;") + .unwrap(); + ast.combine(export_ast); + let _: () = engine.eval_ast_with_scope(&mut scope, &ast).unwrap(); + scope +} + #[tokio::main] async fn main() { - // swag + let opt = Arc::new(Opt::from_args()); + + let engine = make_engine(); + let scope = get_config_scope(&engine, &opt); + + let http_ports: rhai::Array = scope.get_value("http_ports").unwrap(); + let http_ports: Vec = http_ports + .into_iter() + .map(|x| x.as_int().unwrap() as u16) + .collect(); + let https_ports: rhai::Array = scope.get_value("https_ports").unwrap(); + let https_ports: Vec = https_ports + .into_iter() + .map(|x| x.as_int().unwrap() as u16) + .collect(); + + assert!(https_ports.is_empty(), "HTTPS is complicated oops"); + + // TODO learn hyper + let do_response = move |ctx: &NativeCallContext, domains: Map, req: Request| { + for (domain, handler) in &domains { + let req_domain = req.headers().get(header::HOST).unwrap(); + if domain.as_str() == req_domain { + eprintln!("request {:?} matched domain {}", req, domain); + // matched! + let handler: FnPtr = handler.clone_cast(); + let args = [Dynamic::from(Rc::new(req))]; + let result = handler.call_dynamic(ctx, None, args).unwrap(); + let result: Rc> = result.cast(); + return Rc::try_unwrap(result).unwrap(); + } + } + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("Not Found")) + .unwrap() + }; + let addr = http_ports.into_iter().flat_map(|port| { + ["127.0.0.1", "::1"] + .iter() + .map(move |ip| SocketAddr::new(ip.parse().unwrap(), port)) + }); + + let make_svc = make_service_fn(move |_conn| async move { + Ok::<_, Infallible>(service_fn(move |req| async move { + let handle = tokio::runtime::Handle::current(); + let response = handle + .spawn_blocking(move || { + let opt = Opt::from_args(); + let engine = make_engine(); + let scope = get_config_scope(&engine, &opt); + let request_handler_context = + NativeCallContext::new(&engine, "handle_request", &[]); + let domains: Map = scope.get_value("domains").unwrap(); + do_response(&request_handler_context, domains, req) + }) + .await + .unwrap(); + Ok::<_, Infallible>(response) + })) + }); + + // TODO uhh + let mut addr = addr; + let addr = addr.nth(0).unwrap(); + let server = Server::bind(&addr).serve(make_svc); + + if let Err(e) = server.await { + eprintln!("server error: {}", e); + } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..0bd5c2e --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,2 @@ +pub mod proxy_child; +pub mod serve_static; diff --git a/src/utils/proxy_child.rs b/src/utils/proxy_child.rs new file mode 100644 index 0000000..d7a021d --- /dev/null +++ b/src/utils/proxy_child.rs @@ -0,0 +1,74 @@ +use std::process::{Child as ChildProcess, Command}; +use std::rc::Rc; + +use hyper::http::uri::Scheme; +use hyper::{header, Body, Client, Request, Response}; +use rhai::{Dynamic, FnPtr, Map}; + +pub struct KillOnDrop(ChildProcess); + +impl Drop for KillOnDrop { + fn drop(&mut self) { + self.0.kill().unwrap(); + } +} + +#[derive(Clone)] +pub struct ProxyChild { + process: Rc, + port: u16, +} + +impl ProxyChild { + fn new(params: Map) -> Self { + let command_line = params["command"].clone().into_immutable_string().unwrap(); + let port = params["port"].as_int().unwrap(); + let mut command_line = command_line.split(" "); + let command = command_line.next().unwrap(); + let mut child = Command::new(command); + child.args(command_line); + if let Some(cwd) = params.get("in_dir") { + let cwd = cwd.clone().into_immutable_string().unwrap(); + let cwd: &str = cwd.as_ref(); + child.current_dir(cwd); + } + let child = child.spawn().unwrap(); + Self { + process: Rc::new(KillOnDrop(child)), + port: port as u16, + } + } +} + +pub fn handle_request(child: &mut ProxyChild, request: Rc>) -> Rc> { + let ProxyChild { port, .. } = child; + let mut request_uri = request.uri().clone().into_parts(); + // TODO ipv6 loopback? + request_uri.authority = Some(format!("127.0.0.1:{}", port).parse().unwrap()); + request_uri.scheme = Some(Scheme::HTTP); + let mut proxy_request = Request::builder() + .method(request.method()) + .uri(request_uri) + .header(header::HOST, request.headers()[header::HOST].clone()); + proxy_request.headers_mut().unwrap().extend( + request + .headers() + .iter() + .map(|(x, y)| (x.clone(), y.clone())), + ); + // TODO handle nonempty body + let proxy_request = proxy_request.body(Body::empty()).unwrap(); + let response = async { + let client = Client::new(); + Rc::new(client.request(proxy_request).await.unwrap()) + }; + let runtime = tokio::runtime::Handle::current(); + runtime.block_on(response) +} + +pub fn proxy_child(params: Map) -> FnPtr { + let child = ProxyChild::new(params); + let mut result = FnPtr::new("handle_request_proxy_child").unwrap(); + result.add_curry(Dynamic::from(child)); + result +} diff --git a/src/utils/serve_static.rs b/src/utils/serve_static.rs new file mode 100644 index 0000000..2ec0a90 --- /dev/null +++ b/src/utils/serve_static.rs @@ -0,0 +1,75 @@ +use std::fs::File; +use std::io::{ErrorKind, Read}; +use std::path::PathBuf; +use std::rc::Rc; + +use hyper::{Body, Request, Response, StatusCode}; +use rhai::{Dynamic, FnPtr, Map}; + +#[derive(Clone, Debug)] +pub struct Params { + root: PathBuf, +} + +#[derive(Debug)] +pub struct KeyError(&'static str); + +impl TryFrom for Params { + type Error = KeyError; + + fn try_from(mut value: Map) -> Result { + let root = value + .remove("root") + .and_then(|value| value.into_immutable_string().ok()) + .ok_or(KeyError("root"))?; + let root: PathBuf = root.parse().unwrap(); + let root = root.canonicalize().unwrap(); + Ok(Self { root }) + } +} + +pub fn handle_request(params: &mut Params, request: Rc>) -> Rc> { + let Params { root } = params; + eprintln!("handling request {:?}", request); + let request_path = request.uri().path(); + let request_path = request_path.trim_start_matches("/"); + let target = root.join(request_path); + let target = if target.is_dir() { + target.join("index.html") + } else { + target + }; + eprintln!("target is {}", target.display()); + let response = if target.ancestors().all(|ancestor| ancestor != root) { + // oops! all directory traversal + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::empty()) + } else { + let file = File::open(target); + match file { + Ok(mut file) => { + let mut result = Vec::new(); + file.read_to_end(&mut result).unwrap(); + Response::builder().body(Body::from(result)) + } + Err(err) => { + let status_code = match err.kind() { + ErrorKind::NotFound => StatusCode::NOT_FOUND, + _ => StatusCode::BAD_REQUEST, + }; + Response::builder() + .status(status_code) + .body(Body::from(status_code.canonical_reason().unwrap_or(""))) + } + } + }; + Rc::new(response.unwrap()) +} + +pub fn serve_static(params: Map) -> FnPtr { + let params: Params = params.try_into().unwrap(); + let mut result = FnPtr::new("handle_request_serve_static").unwrap(); + result.add_curry(Dynamic::from(params)); + result +} diff --git a/tests/config-example/main.rs b/tests/config-example/main.rs index ed0f863..4b02c58 100644 --- a/tests/config-example/main.rs +++ b/tests/config-example/main.rs @@ -4,10 +4,18 @@ use std::process::Command; use hyper::{body::aggregate, body::Buf, Body, Client, Method, Request, Uri}; +#[path = "../lib.rs"] +mod helpers; +use helpers::ChildExt; + #[tokio::test] async fn main() { let narchttpd_path = env!("CARGO_BIN_EXE_narchttpd"); - let mut narchttpd = Command::new(narchttpd_path).spawn().unwrap(); + let _narchttpd = Command::new(narchttpd_path) + .current_dir("tests/config-example") + .spawn() + .unwrap() + .kill_on_drop(); let client = Client::new(); @@ -24,11 +32,11 @@ async fn main() { .body(Body::empty()) .unwrap(); let res = client.request(req).await.unwrap(); - assert!(res.status().is_success()); + assert!(res.status().is_success(), "{:?}", res); let body = aggregate(res.into_body()).await.unwrap(); let mut result = String::new(); body.reader().read_to_string(&mut result).unwrap(); - result + result.trim().to_string() }; assert_eq!( @@ -45,6 +53,4 @@ async fn main() { ); assert!(get("http://dynamic.test").await.contains("hello-there.txt")); assert_eq!(get("http://function.test").await, "OK"); - - narchttpd.kill().unwrap(); } diff --git a/tests/config-example/narchttpd.rhai b/tests/config-example/narchttpd.rhai index c876bc1..7c9185d 100644 --- a/tests/config-example/narchttpd.rhai +++ b/tests/config-example/narchttpd.rhai @@ -1,17 +1,20 @@ http_ports = [1337]; https_ports = []; -domain(["domain.test", "alternate-domain.test"], serve_static(#{ +let sample = serve_static(#{ root: "./domain.test", -})); -domain("sub.domain.test", serve_static(#{ +}); +domains["domain.test"] = sample; +domains["alternate-domain.test"] = sample; +domains["sub.domain.test"] = serve_static(#{ root: "./sub.domain.test", -})); -domain("dynamic.test", proxy_child(#{ - command: |port| "python -m http.server " + port, +}); +domains["dynamic.test"] = proxy_child(#{ + command: "python -m http.server 6970", in_dir: "./dynamic.test", -})); -domain("function.test", |request| #{ + port: 6970, +}); +domains["function.test"] = |request| #{ status: 200, body: "OK", -}); +}; diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..50d4fa7 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,19 @@ +use std::process::Child; + +pub struct KillOnDrop(Child); + +impl Drop for KillOnDrop { + fn drop(&mut self) { + self.0.kill().unwrap(); + } +} + +pub trait ChildExt { + fn kill_on_drop(self) -> KillOnDrop; +} + +impl ChildExt for Child { + fn kill_on_drop(self) -> KillOnDrop { + KillOnDrop(self) + } +} -- cgit v1.2.3