aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMelody Horn <melody@boringcactus.com>2021-11-12 22:38:39 -0700
committerMelody Horn <melody@boringcactus.com>2021-11-12 22:38:39 -0700
commitf79b75710167088e2031a82a94a8d127fbb16a1f (patch)
treeffbbd167c34864d92b1d1e0a3b40da77e0b06874
parentfad6b7b755b788ca0d9113999faa8fb0d6f89ad0 (diff)
downloadnarchttpd-f79b75710167088e2031a82a94a8d127fbb16a1f.tar.gz
narchttpd-f79b75710167088e2031a82a94a8d127fbb16a1f.zip
god is dead and we have killed him.
-rw-r--r--Cargo.lock126
-rw-r--r--Cargo.toml3
-rw-r--r--src/main.rs122
-rw-r--r--src/utils.rs2
-rw-r--r--src/utils/proxy_child.rs74
-rw-r--r--src/utils/serve_static.rs75
-rw-r--r--tests/config-example/main.rs16
-rw-r--r--tests/config-example/narchttpd.rhai21
-rw-r--r--tests/lib.rs19
9 files changed, 442 insertions, 16 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ad40ff6..78bbea7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -14,6 +14,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -38,6 +58,21 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -120,6 +155,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -275,6 +319,7 @@ version = "0.1.0"
dependencies = [
"hyper",
"rhai",
+ "structopt",
"tokio",
]
@@ -350,6 +395,30 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -455,6 +524,36 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -466,6 +565,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -543,12 +651,30 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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::<Rc<Request<Body>>>("Request");
+ engine.register_type::<utils::serve_static::Params>();
+ 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<u16> = 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<u16> = 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<Body>| {
+ 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<Response<Body>> = 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<KillOnDrop>,
+ 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<Request<Body>>) -> Rc<Response<Body>> {
+ 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<Map> for Params {
+ type Error = KeyError;
+
+ fn try_from(mut value: Map) -> Result<Self, Self::Error> {
+ 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<Request<Body>>) -> Rc<Response<Body>> {
+ 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)
+ }
+}