diff --git a/Cargo.lock b/Cargo.lock index 2d72e85..ebde8c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1477,9 +1477,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "preserves" -version = "4.992.0" +version = "4.992.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d1499a990075d8c1aa3f6550da8a6d6542224c9375c6908f9325f757c38a7a" +checksum = "363e99221abed81abac2cc518740859349c354504086bc1638c58b29c1603f5e" dependencies = [ "base64", "dtoa", diff --git a/http-config.pr b/http-config.pr new file mode 100644 index 0000000..a54be0a --- /dev/null +++ b/http-config.pr @@ -0,0 +1,65 @@ +# We use $root_ds as the httpd space. +let ?root_ds = dataspace + +# Supplying $root_ds as the last parameter in this relay-listener enables httpd service. + $gatekeeper $root_ds>> + +# Regular gatekeeper stuff works too. + $root_ds #f> + +# Create an httpd router monitoring $root_ds for requests and bind requests. +> + +# Create a static file server. When it gets a request, it ignores the first n (here, 1) +# elements of the path, and takes the remainder as relative to its configured directory (here, +# "."). +# +> +# +# It publishes a service object: requests should be asserted to this. +# The http-bind record establishes this mapping. +# +? ?handler> [ + $root_ds += +] + +# Separately, bind path /d to $index, and respond there. +# +let ?index = dataspace +$root_ds += +$index ? [ + $k ! + $k !
+ $k ! "> + $k ! D"> +] + +# Similarly, bind three paths, /d, /e and /t to $index2 +# Because /d doubles up, the httpd router gives a warning when it is accessed. +# Accessing /e works fine. +# Accessing /t results in wasted work because of the hijacking listeners below. +# +let ?index2 = dataspace +$root_ds += +$root_ds += +$root_ds += +$index2 ? [ + $k ! + $k !
+ $k ! "> + $k ! D2"> +] + +# These two hijack /t by listening for raw incoming requests the same way the httpd router +# does. They respond quicker and so win the race. The httpd router's responses are lost. +# +$root_ds ? ?k> [ + $k ! + $k !
+ $k ! T"> +] +$root_ds ? ?k> [ + $k ! + $k !
+ $k ! T2"> +] diff --git a/syndicate-server/Cargo.toml b/syndicate-server/Cargo.toml index 8d83d30..70068d1 100644 --- a/syndicate-server/Cargo.toml +++ b/syndicate-server/Cargo.toml @@ -13,12 +13,12 @@ license = "Apache-2.0" jemalloc = ["dep:tikv-jemallocator"] [build-dependencies] -preserves-schema = "4.991" +preserves-schema = "4.992" syndicate = { path = "../syndicate", version = "0.30.0"} syndicate-schema-plugin = { path = "../syndicate-schema-plugin", version = "0.2.0"} [dependencies] -preserves-schema = "4.991" +preserves-schema = "4.992" syndicate = { path = "../syndicate", version = "0.30.0"} syndicate-macros = { path = "../syndicate-macros", version = "0.25.0"} diff --git a/syndicate-server/protocols/schema-bundle.bin b/syndicate-server/protocols/schema-bundle.bin index 640b836..1f7be8a 100644 --- a/syndicate-server/protocols/schema-bundle.bin +++ b/syndicate-server/protocols/schema-bundle.bin @@ -10,4 +10,4 @@ gatekeeper gatekeeper´³embedded´³refµ³ gatekeeper„³Resolve„„„„„„³TcpRelayListener´³orµµ±TcpWithoutHttp´³refµ„³TcpWithoutHttp„„µ± TcpWithHttp´³refµ„³ TcpWithHttp„„„„³UnixRelayListener´³rec´³lit³relay-listener„´³tupleµ´³named³addr´³refµ³TransportAddress„³Unix„„´³named³ gatekeeper´³embedded´³refµ³ -gatekeeper„³Resolve„„„„„„„³ embeddedType´³refµ³ EntityRef„³Cap„„„„„ \ No newline at end of file +gatekeeper„³Resolve„„„„„„³HttpStaticFileServer´³rec´³lit³http-static-files„´³tupleµ´³named³dir´³atom³String„„´³named³pathPrefixElements´³atom³ SignedInteger„„„„„„³ embeddedType´³refµ³ EntityRef„³Cap„„„„„ \ No newline at end of file diff --git a/syndicate-server/protocols/schemas/internalServices.prs b/syndicate-server/protocols/schemas/internalServices.prs index 189c23f..7f39243 100644 --- a/syndicate-server/protocols/schemas/internalServices.prs +++ b/syndicate-server/protocols/schemas/internalServices.prs @@ -13,3 +13,4 @@ ConfigWatcher = . ConfigEnv = { symbol: any ...:... }. HttpRouter = . +HttpStaticFileServer = . diff --git a/syndicate-server/src/services/http_router.rs b/syndicate-server/src/services/http_router.rs index 0f72ad9..0dbba0b 100644 --- a/syndicate-server/src/services/http_router.rs +++ b/syndicate-server/src/services/http_router.rs @@ -1,5 +1,7 @@ use preserves_schema::Codec; +use std::convert::TryFrom; +use std::io::Read; use std::sync::Arc; use syndicate::actor::*; @@ -10,47 +12,80 @@ use syndicate::preserves::value::Map; use syndicate::preserves::value::NestedValue; use syndicate::preserves::value::Set; use syndicate::schemas::http; +use syndicate::value::signed_integer::SignedInteger; use crate::language::language; use crate::lifecycle; use crate::schemas::internal_services::HttpRouter; +use crate::schemas::internal_services::HttpStaticFileServer; use syndicate_macros::during; +lazy_static::lazy_static! { + pub static ref MIME_TABLE: Map = load_mime_table("/etc/mime.types").expect("MIME table"); +} + +pub fn load_mime_table(path: &str) -> Result, std::io::Error> { + let mut table = Map::new(); + let file = std::fs::read_to_string(path)?; + for line in file.split('\n') { + if line.starts_with('#') { + continue; + } + let pieces = line.split(&[' ', '\t'][..]).collect::>(); + for i in 1..pieces.len() { + table.insert(pieces[i].to_string(), pieces[0].to_string()); + } + } + Ok(table) +} + pub fn on_demand(t: &mut Activation, ds: Arc) { t.spawn(Some(AnyValue::symbol("http_router_listener")), move |t| { - Ok(during!(t, ds, language(), >, |t: &mut Activation| { + enclose!((ds) during!(t, ds, language(), >, |t: &mut Activation| { t.spawn_link(Some(rec![AnyValue::symbol("http_router"), language().unparse(&spec)]), enclose!((ds) |t| run(t, ds, spec))); Ok(()) - })) + })); + enclose!((ds) during!(t, ds, language(), , |t: &mut Activation| { + t.spawn_link(Some(rec![AnyValue::symbol("http_static_file_server"), language().unparse(&spec)]), + enclose!((ds) |t| run_static_file_server(t, ds, spec))); + Ok(()) + })); + Ok(()) }); } type MethodTable = Map>>; -type RoutingTable = Map>; +type HostTable = Map>; +type RoutingTable = Map; fn run(t: &mut Activation, ds: Arc, spec: HttpRouter) -> ActorResult { ds.assert(t, language(), &lifecycle::started(&spec)); ds.assert(t, language(), &lifecycle::ready(&spec)); let httpd = spec.httpd; - during!(t, httpd, language(), , |t: &mut Activation| { - let routes: Arc> = t.named_field("routes", Map::new()); + let routes: Arc> = t.named_field("routes", Map::new()); + + enclose!((httpd, routes) during!(t, httpd, language(), , |t: &mut Activation| { let port1 = port.clone(); - enclose!((httpd, routes) during!(t, httpd, language(), , enclose!((routes) |t: &mut Activation| { - during!(t, httpd, language(), , |t: &mut Activation| { + enclose!((httpd, routes) during!(t, httpd, language(), , enclose!((routes, port) |t: &mut Activation| { + let port2 = port.clone(); + during!(t, httpd, language(), , |t: &mut Activation| { + let port = port.value().to_signedinteger()?; let host = language().parse::(&host)?; let path = language().parse::(&path)?; let method = language().parse::(&method)?; let handler = handler.value().to_embedded()?; t.get_mut(&routes) + .entry(port.clone()).or_default() .entry(host.clone()).or_default() .entry(path.clone()).or_default() .entry(method.clone()).or_default() .insert(handler.clone()); - t.on_stop(enclose!((routes, handler, method, path, host) move |t| { - let host_map = t.get_mut(&routes); + t.on_stop(enclose!((routes, handler, method, path, host, port) move |t| { + let port_map = t.get_mut(&routes); + let host_map = port_map.entry(port.clone()).or_default(); let path_map = host_map.entry(host.clone()).or_default(); let method_map = path_map.entry(path.clone()).or_default(); let handler_set = method_map.entry(method.clone()).or_default(); @@ -64,64 +99,75 @@ fn run(t: &mut Activation, ds: Arc, spec: HttpRouter) -> ActorResult { if path_map.is_empty() { host_map.remove(&host); } + if host_map.is_empty() { + port_map.remove(&port); + } Ok(()) })); Ok(()) }); Ok(()) }))); - during!(t, httpd, language(), , |t: &mut Activation| { - let req = match language().parse::(&req) { Ok(v) => v, Err(_) => return Ok(()) }; - let res = match res.value().to_embedded() { Ok(v) => v, Err(_) => return Ok(()) }; + Ok(()) + })); - let methods = match try_hostname(t, &routes, http::HostPattern::Host(req.host.clone()), &req.path)? { + during!(t, httpd, language(), , |t: &mut Activation| { + let req = match language().parse::(&req) { Ok(v) => v, Err(_) => return Ok(()) }; + let res = match res.value().to_embedded() { Ok(v) => v, Err(_) => return Ok(()) }; + + let host_map = match t.get(&routes).get(&req.port) { + Some(host_map) => host_map, + None => return send_empty(t, res, 404, "Not found"), + }; + + let methods = match try_hostname(host_map, http::HostPattern::Host(req.host.clone()), &req.path)? { + Some(methods) => methods, + None => match try_hostname(host_map, http::HostPattern::Any, &req.path)? { Some(methods) => methods, - None => match try_hostname(t, &routes, http::HostPattern::Any, &req.path)? { - Some(methods) => methods, - None => { - res.message(t, language(), &http::HttpResponse::Status { - code: 404.into(), message: "Not found".into() }); - res.message(t, language(), &http::HttpResponse::Done { - chunk: Box::new(http::Chunk::Bytes(vec![])) }); - return Ok(()) - } - } - }; - - let handlers = match methods.get(&http::MethodPattern::Specific(req.method.clone())) { - Some(handlers) => handlers, - None => match methods.get(&http::MethodPattern::Any) { - Some(handlers) => handlers, - None => { - let allowed = methods.keys().map(|k| match k { - http::MethodPattern::Specific(m) => m.to_uppercase(), - http::MethodPattern::Any => unreachable!(), - }).collect::>().join(", "); - res.message(t, language(), &http::HttpResponse::Status { - code: 405.into(), message: "Method Not Allowed".into() }); - res.message(t, language(), &http::HttpResponse::Header { - name: "allow".into(), value: allowed }); - res.message(t, language(), &http::HttpResponse::Done { - chunk: Box::new(http::Chunk::Bytes(vec![])) }); - return Ok(()) - } - } - }; - - if handlers.len() > 1 { - tracing::warn!(?req, "Too many handlers available"); + None => return send_empty(t, res, 404, "Not found"), } - let handler = handlers.first().expect("Nonempty handler set").clone(); - handler.assert(t, language(), &http::HttpContext { req, res: res.clone() }); + }; + + let handlers = match methods.get(&http::MethodPattern::Specific(req.method.clone())) { + Some(handlers) => handlers, + None => match methods.get(&http::MethodPattern::Any) { + Some(handlers) => handlers, + None => { + let allowed = methods.keys().map(|k| match k { + http::MethodPattern::Specific(m) => m.to_uppercase(), + http::MethodPattern::Any => unreachable!(), + }).collect::>().join(", "); + res.message(t, language(), &http::HttpResponse::Status { + code: 405.into(), message: "Method Not Allowed".into() }); + res.message(t, language(), &http::HttpResponse::Header { + name: "allow".into(), value: allowed }); + res.message(t, language(), &http::HttpResponse::Done { + chunk: Box::new(http::Chunk::Bytes(vec![])) }); + return Ok(()) + } + } + }; + + if handlers.len() > 1 { + tracing::warn!(?req, "Too many handlers available"); + } + let handler = handlers.first().expect("Nonempty handler set").clone(); + handler.assert(t, language(), &http::HttpContext { req, res: res.clone() }); - Ok(()) - }); Ok(()) }); Ok(()) } +fn send_empty(t: &mut Activation, res: &Arc, code: u16, message: &str) -> ActorResult { + res.message(t, language(), &http::HttpResponse::Status { + code: code.into(), message: message.into() }); + res.message(t, language(), &http::HttpResponse::Done { + chunk: Box::new(http::Chunk::Bytes(vec![])) }); + return Ok(()) +} + fn path_pattern_matches(path_pat: &http::PathPattern, path: &Vec) -> bool { let mut path_iter = path.iter(); for pat_elem in path_pat.0.iter() { @@ -144,13 +190,12 @@ fn path_pattern_matches(path_pat: &http::PathPattern, path: &Vec) -> boo true } -fn try_hostname<'turn, 'routes>( - t: &'routes mut Activation<'turn>, - routes: &'routes Arc>, +fn try_hostname<'table>( + host_map: &'table HostTable, host_pat: http::HostPattern, path: &Vec, -) -> Result, Error> { - match t.get(routes).get(&host_pat) { +) -> Result, Error> { + match host_map.get(&host_pat) { None => Ok(None), Some(path_table) => { for (path_pat, method_table) in path_table.iter() { @@ -162,3 +207,105 @@ fn try_hostname<'turn, 'routes>( } } } + +fn render_dir(path: std::path::PathBuf) -> Result<(Vec, Option<&'static str>), Error> { + let mut body = String::new(); + for entry in std::fs::read_dir(&path)? { + if let Ok(entry) = entry { + let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or(false); + let name = entry.file_name().to_string_lossy() + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('\'', "'") + .replace('"', """) + (if is_dir { "/" } else { "" }); + body.push_str(&format!("{}
\n", name, name)); + } + } + Ok((body.into_bytes(), Some("text/html"))) +} + +impl HttpStaticFileServer { + fn respond(&mut self, t: &mut Activation, req: &http::HttpRequest, res: &Arc) -> ActorResult { + let path_prefix_elements = usize::try_from(&self.path_prefix_elements) + .map_err(|_| "Bad pathPrefixElements")?; + let mut is_index = false; + + let mut path = req.path[path_prefix_elements..].iter().cloned().collect::>(); + if let Some(e) = path.last_mut() { + if e.len() == 0 { + *e = "index.html".into(); + is_index = true; + } + } + + let mut realpath = std::path::PathBuf::from(&self.dir); + for element in path.into_iter() { + if element.contains('/') || element.starts_with('.') { Err("Invalid path element")?; } + realpath.push(element); + } + + let (body, mime_type) = match std::fs::File::open(&realpath) { + Err(_) => { + if is_index { + realpath.pop(); + } + if std::fs::metadata(&realpath).is_ok_and(|m| m.is_dir()) { + render_dir(realpath)? + } else { + return send_empty(t, res, 404, "Not found") + } + }, + Ok(mut fh) => { + if fh.metadata().is_ok_and(|m| m.is_dir()) { + drop(fh); + res.message(t, language(), &http::HttpResponse::Status { + code: 301.into(), message: "Moved permanently".into() }); + res.message(t, language(), &http::HttpResponse::Header { + name: "location".into(), value: format!("/{}/", req.path.join("/")) }); + res.message(t, language(), &http::HttpResponse::Done { + chunk: Box::new(http::Chunk::Bytes(vec![])) }); + return Ok(()) + } else { + let mut buf = Vec::new(); + fh.read_to_end(&mut buf)?; + if let Some(extension) = realpath.extension().and_then(|e| e.to_str()) { + (buf, MIME_TABLE.get(extension).map(|m| m.as_str())) + } else { + (buf, None) + } + } + } + }; + + res.message(t, language(), &http::HttpResponse::Status { + code: 200.into(), message: "OK".into() }); + if let Some(mime_type) = mime_type { + res.message(t, language(), &http::HttpResponse::Header { + name: "content-type".into(), value: mime_type.to_owned() }); + } + res.message(t, language(), &http::HttpResponse::Done { + chunk: Box::new(http::Chunk::Bytes(body)) }); + Ok(()) + } +} + +impl Entity> for HttpStaticFileServer { + fn assert(&mut self, t: &mut Activation, assertion: http::HttpContext, _handle: Handle) -> ActorResult { + let http::HttpContext { req, res } = assertion; + if let Err(e) = self.respond(t, &req, &res) { + tracing::error!(?req, error=?e); + send_empty(t, &res, 500, "Internal server error")?; + } + Ok(()) + } +} + +fn run_static_file_server(t: &mut Activation, ds: Arc, spec: HttpStaticFileServer) -> ActorResult { + let object = Cap::guard(&language().syndicate, t.create(spec.clone())); + ds.assert(t, language(), &syndicate::schemas::service::ServiceObject { + service_name: language().unparse(&spec), + object: AnyValue::domain(object), + }); + Ok(()) +} diff --git a/syndicate-tools/Cargo.toml b/syndicate-tools/Cargo.toml index 36bbaeb..3dd62dc 100644 --- a/syndicate-tools/Cargo.toml +++ b/syndicate-tools/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://git.syndicate-lang.org/syndicate-lang/syndicate-rs" license = "Apache-2.0" [dependencies] -preserves = "4.991" +preserves = "4.992" syndicate = { path = "../syndicate", version = "0.30.0"} clap = { version = "^4.0", features = ["derive"] } diff --git a/syndicate/Cargo.toml b/syndicate/Cargo.toml index 166f25a..f6d761b 100644 --- a/syndicate/Cargo.toml +++ b/syndicate/Cargo.toml @@ -13,11 +13,11 @@ license = "Apache-2.0" vendored-openssl = ["openssl/vendored"] [build-dependencies] -preserves-schema = "4.991" +preserves-schema = "4.992" [dependencies] -preserves = "4.991" -preserves-schema = "4.991" +preserves = "4.992" +preserves-schema = "4.992" tokio = { version = "1.10", features = ["io-util", "macros", "rt", "rt-multi-thread", "time"] } tokio-util = "0.6"