Static file service
This commit is contained in:
parent
65dae05890
commit
a38765affa
|
@ -1477,9 +1477,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "preserves"
|
name = "preserves"
|
||||||
version = "4.992.0"
|
version = "4.992.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23d1499a990075d8c1aa3f6550da8a6d6542224c9375c6908f9325f757c38a7a"
|
checksum = "363e99221abed81abac2cc518740859349c354504086bc1638c58b29c1603f5e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"dtoa",
|
"dtoa",
|
||||||
|
|
|
@ -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.
|
||||||
|
<require-service <relay-listener <tcp "0.0.0.0" 9001> $gatekeeper $root_ds>>
|
||||||
|
|
||||||
|
# Regular gatekeeper stuff works too.
|
||||||
|
<bind <ref { oid: "syndicate" key: #x"" }> $root_ds #f>
|
||||||
|
|
||||||
|
# Create an httpd router monitoring $root_ds for requests and bind requests.
|
||||||
|
<require-service <http-router $root_ds>>
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
# ".").
|
||||||
|
#
|
||||||
|
<require-service <http-static-files "." 1>>
|
||||||
|
#
|
||||||
|
# It publishes a service object: requests should be asserted to this.
|
||||||
|
# The http-bind record establishes this mapping.
|
||||||
|
#
|
||||||
|
? <service-object <http-static-files "." 1> ?handler> [
|
||||||
|
$root_ds += <http-bind #f 9001 get ["files" ...] $handler>
|
||||||
|
]
|
||||||
|
|
||||||
|
# Separately, bind path /d to $index, and respond there.
|
||||||
|
#
|
||||||
|
let ?index = dataspace
|
||||||
|
$root_ds += <http-bind #f 9001 get ["d"] $index>
|
||||||
|
$index ? <request _ ?k> [
|
||||||
|
$k ! <status 200 "OK">
|
||||||
|
$k ! <header content-type "text/html">
|
||||||
|
$k ! <chunk "<!DOCTYPE html>">
|
||||||
|
$k ! <done "<html><body>D</body></html>">
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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 += <http-bind #f 9001 get ["d"] $index2>
|
||||||
|
$root_ds += <http-bind #f 9001 get ["e"] $index2>
|
||||||
|
$root_ds += <http-bind #f 9001 get ["t"] $index2>
|
||||||
|
$index2 ? <request _ ?k> [
|
||||||
|
$k ! <status 200 "OK">
|
||||||
|
$k ! <header content-type "text/html">
|
||||||
|
$k ! <chunk "<!DOCTYPE html>">
|
||||||
|
$k ! <done "<html><body>D2</body></html>">
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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 ? <request <http-request _ _ _ get ["t"] _ _ _> ?k> [
|
||||||
|
$k ! <status 200 "OK">
|
||||||
|
$k ! <header content-type "text/html">
|
||||||
|
$k ! <done "<html><body>T</body></html>">
|
||||||
|
]
|
||||||
|
$root_ds ? <request <http-request _ _ _ get ["t"] _ _ _> ?k> [
|
||||||
|
$k ! <status 200 "OK">
|
||||||
|
$k ! <header content-type "text/html">
|
||||||
|
$k ! <done "<html><body>T2</body></html>">
|
||||||
|
]
|
|
@ -13,12 +13,12 @@ license = "Apache-2.0"
|
||||||
jemalloc = ["dep:tikv-jemallocator"]
|
jemalloc = ["dep:tikv-jemallocator"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
preserves-schema = "4.991"
|
preserves-schema = "4.992"
|
||||||
syndicate = { path = "../syndicate", version = "0.30.0"}
|
syndicate = { path = "../syndicate", version = "0.30.0"}
|
||||||
syndicate-schema-plugin = { path = "../syndicate-schema-plugin", version = "0.2.0"}
|
syndicate-schema-plugin = { path = "../syndicate-schema-plugin", version = "0.2.0"}
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
preserves-schema = "4.991"
|
preserves-schema = "4.992"
|
||||||
syndicate = { path = "../syndicate", version = "0.30.0"}
|
syndicate = { path = "../syndicate", version = "0.30.0"}
|
||||||
syndicate-macros = { path = "../syndicate-macros", version = "0.25.0"}
|
syndicate-macros = { path = "../syndicate-macros", version = "0.25.0"}
|
||||||
|
|
||||||
|
|
|
@ -10,4 +10,4 @@ gatekeeper
|
||||||
gatekeeper´³embedded´³refµ³
|
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„³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´³embedded´³refµ³
|
||||||
gatekeeper„³Resolve„„„„„„„³embeddedType´³refµ³ EntityRef„³Cap„„„„„
|
gatekeeper„³Resolve„„„„„„³HttpStaticFileServer´³rec´³lit³http-static-files„´³tupleµ´³named³dir´³atom³String„„´³named³pathPrefixElements´³atom³
SignedInteger„„„„„„³embeddedType´³refµ³ EntityRef„³Cap„„„„„
|
|
@ -13,3 +13,4 @@ ConfigWatcher = <config-watcher @path string @env ConfigEnv>.
|
||||||
ConfigEnv = { symbol: any ...:... }.
|
ConfigEnv = { symbol: any ...:... }.
|
||||||
|
|
||||||
HttpRouter = <http-router @httpd #!any> .
|
HttpRouter = <http-router @httpd #!any> .
|
||||||
|
HttpStaticFileServer = <http-static-files @dir string @pathPrefixElements int> .
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use preserves_schema::Codec;
|
use preserves_schema::Codec;
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::io::Read;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use syndicate::actor::*;
|
use syndicate::actor::*;
|
||||||
|
@ -10,47 +12,80 @@ use syndicate::preserves::value::Map;
|
||||||
use syndicate::preserves::value::NestedValue;
|
use syndicate::preserves::value::NestedValue;
|
||||||
use syndicate::preserves::value::Set;
|
use syndicate::preserves::value::Set;
|
||||||
use syndicate::schemas::http;
|
use syndicate::schemas::http;
|
||||||
|
use syndicate::value::signed_integer::SignedInteger;
|
||||||
|
|
||||||
use crate::language::language;
|
use crate::language::language;
|
||||||
use crate::lifecycle;
|
use crate::lifecycle;
|
||||||
use crate::schemas::internal_services::HttpRouter;
|
use crate::schemas::internal_services::HttpRouter;
|
||||||
|
use crate::schemas::internal_services::HttpStaticFileServer;
|
||||||
|
|
||||||
use syndicate_macros::during;
|
use syndicate_macros::during;
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
pub static ref MIME_TABLE: Map<String, String> = load_mime_table("/etc/mime.types").expect("MIME table");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_mime_table(path: &str) -> Result<Map<String, String>, 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::<Vec<&str>>();
|
||||||
|
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<Cap>) {
|
pub fn on_demand(t: &mut Activation, ds: Arc<Cap>) {
|
||||||
t.spawn(Some(AnyValue::symbol("http_router_listener")), move |t| {
|
t.spawn(Some(AnyValue::symbol("http_router_listener")), move |t| {
|
||||||
Ok(during!(t, ds, language(), <run-service $spec: HttpRouter::<AnyValue>>, |t: &mut Activation| {
|
enclose!((ds) during!(t, ds, language(), <run-service $spec: HttpRouter::<AnyValue>>, |t: &mut Activation| {
|
||||||
t.spawn_link(Some(rec![AnyValue::symbol("http_router"), language().unparse(&spec)]),
|
t.spawn_link(Some(rec![AnyValue::symbol("http_router"), language().unparse(&spec)]),
|
||||||
enclose!((ds) |t| run(t, ds, spec)));
|
enclose!((ds) |t| run(t, ds, spec)));
|
||||||
Ok(())
|
Ok(())
|
||||||
}))
|
}));
|
||||||
|
enclose!((ds) during!(t, ds, language(), <run-service $spec: HttpStaticFileServer>, |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<http::MethodPattern, Set<Arc<Cap>>>;
|
type MethodTable = Map<http::MethodPattern, Set<Arc<Cap>>>;
|
||||||
type RoutingTable = Map<http::HostPattern, Map<http::PathPattern, MethodTable>>;
|
type HostTable = Map<http::HostPattern, Map<http::PathPattern, MethodTable>>;
|
||||||
|
type RoutingTable = Map<SignedInteger, HostTable>;
|
||||||
|
|
||||||
fn run(t: &mut Activation, ds: Arc<Cap>, spec: HttpRouter) -> ActorResult {
|
fn run(t: &mut Activation, ds: Arc<Cap>, spec: HttpRouter) -> ActorResult {
|
||||||
ds.assert(t, language(), &lifecycle::started(&spec));
|
ds.assert(t, language(), &lifecycle::started(&spec));
|
||||||
ds.assert(t, language(), &lifecycle::ready(&spec));
|
ds.assert(t, language(), &lifecycle::ready(&spec));
|
||||||
let httpd = spec.httpd;
|
let httpd = spec.httpd;
|
||||||
|
|
||||||
during!(t, httpd, language(), <http-bind _ $port _ _ _>, |t: &mut Activation| {
|
let routes: Arc<Field<RoutingTable>> = t.named_field("routes", Map::new());
|
||||||
let routes: Arc<Field<RoutingTable>> = t.named_field("routes", Map::new());
|
|
||||||
|
enclose!((httpd, routes) during!(t, httpd, language(), <http-bind _ $port _ _ _>, |t: &mut Activation| {
|
||||||
let port1 = port.clone();
|
let port1 = port.clone();
|
||||||
enclose!((httpd, routes) during!(t, httpd, language(), <http-listener #(&port1)>, enclose!((routes) |t: &mut Activation| {
|
enclose!((httpd, routes) during!(t, httpd, language(), <http-listener #(&port1)>, enclose!((routes, port) |t: &mut Activation| {
|
||||||
during!(t, httpd, language(), <http-bind $host #(&port) $method $path $handler>, |t: &mut Activation| {
|
let port2 = port.clone();
|
||||||
|
during!(t, httpd, language(), <http-bind $host #(&port2) $method $path $handler>, |t: &mut Activation| {
|
||||||
|
let port = port.value().to_signedinteger()?;
|
||||||
let host = language().parse::<http::HostPattern>(&host)?;
|
let host = language().parse::<http::HostPattern>(&host)?;
|
||||||
let path = language().parse::<http::PathPattern>(&path)?;
|
let path = language().parse::<http::PathPattern>(&path)?;
|
||||||
let method = language().parse::<http::MethodPattern>(&method)?;
|
let method = language().parse::<http::MethodPattern>(&method)?;
|
||||||
let handler = handler.value().to_embedded()?;
|
let handler = handler.value().to_embedded()?;
|
||||||
t.get_mut(&routes)
|
t.get_mut(&routes)
|
||||||
|
.entry(port.clone()).or_default()
|
||||||
.entry(host.clone()).or_default()
|
.entry(host.clone()).or_default()
|
||||||
.entry(path.clone()).or_default()
|
.entry(path.clone()).or_default()
|
||||||
.entry(method.clone()).or_default()
|
.entry(method.clone()).or_default()
|
||||||
.insert(handler.clone());
|
.insert(handler.clone());
|
||||||
t.on_stop(enclose!((routes, handler, method, path, host) move |t| {
|
t.on_stop(enclose!((routes, handler, method, path, host, port) move |t| {
|
||||||
let host_map = t.get_mut(&routes);
|
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 path_map = host_map.entry(host.clone()).or_default();
|
||||||
let method_map = path_map.entry(path.clone()).or_default();
|
let method_map = path_map.entry(path.clone()).or_default();
|
||||||
let handler_set = method_map.entry(method.clone()).or_default();
|
let handler_set = method_map.entry(method.clone()).or_default();
|
||||||
|
@ -64,64 +99,75 @@ fn run(t: &mut Activation, ds: Arc<Cap>, spec: HttpRouter) -> ActorResult {
|
||||||
if path_map.is_empty() {
|
if path_map.is_empty() {
|
||||||
host_map.remove(&host);
|
host_map.remove(&host);
|
||||||
}
|
}
|
||||||
|
if host_map.is_empty() {
|
||||||
|
port_map.remove(&port);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
})));
|
})));
|
||||||
during!(t, httpd, language(), <request $req $res>, |t: &mut Activation| {
|
Ok(())
|
||||||
let req = match language().parse::<http::HttpRequest>(&req) { Ok(v) => v, Err(_) => return Ok(()) };
|
}));
|
||||||
let res = match res.value().to_embedded() { Ok(v) => v, Err(_) => return Ok(()) };
|
|
||||||
|
|
||||||
let methods = match try_hostname(t, &routes, http::HostPattern::Host(req.host.clone()), &req.path)? {
|
during!(t, httpd, language(), <request $req $res>, |t: &mut Activation| {
|
||||||
|
let req = match language().parse::<http::HttpRequest>(&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,
|
Some(methods) => methods,
|
||||||
None => match try_hostname(t, &routes, http::HostPattern::Any, &req.path)? {
|
None => return send_empty(t, res, 404, "Not found"),
|
||||||
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::<Vec<String>>().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() });
|
|
||||||
|
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::<Vec<String>>().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(())
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_empty(t: &mut Activation, res: &Arc<Cap>, 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<String>) -> bool {
|
fn path_pattern_matches(path_pat: &http::PathPattern, path: &Vec<String>) -> bool {
|
||||||
let mut path_iter = path.iter();
|
let mut path_iter = path.iter();
|
||||||
for pat_elem in path_pat.0.iter() {
|
for pat_elem in path_pat.0.iter() {
|
||||||
|
@ -144,13 +190,12 @@ fn path_pattern_matches(path_pat: &http::PathPattern, path: &Vec<String>) -> boo
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_hostname<'turn, 'routes>(
|
fn try_hostname<'table>(
|
||||||
t: &'routes mut Activation<'turn>,
|
host_map: &'table HostTable,
|
||||||
routes: &'routes Arc<Field<RoutingTable>>,
|
|
||||||
host_pat: http::HostPattern,
|
host_pat: http::HostPattern,
|
||||||
path: &Vec<String>,
|
path: &Vec<String>,
|
||||||
) -> Result<Option<&'routes MethodTable>, Error> {
|
) -> Result<Option<&'table MethodTable>, Error> {
|
||||||
match t.get(routes).get(&host_pat) {
|
match host_map.get(&host_pat) {
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
Some(path_table) => {
|
Some(path_table) => {
|
||||||
for (path_pat, method_table) in path_table.iter() {
|
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<u8>, 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!("<a href=\"{}\">{}</a><br>\n", name, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((body.into_bytes(), Some("text/html")))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpStaticFileServer {
|
||||||
|
fn respond(&mut self, t: &mut Activation, req: &http::HttpRequest, res: &Arc<Cap>) -> 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::<Vec<String>>();
|
||||||
|
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<http::HttpContext<AnyValue>> for HttpStaticFileServer {
|
||||||
|
fn assert(&mut self, t: &mut Activation, assertion: http::HttpContext<AnyValue>, _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<Cap>, 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(())
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ repository = "https://git.syndicate-lang.org/syndicate-lang/syndicate-rs"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
preserves = "4.991"
|
preserves = "4.992"
|
||||||
syndicate = { path = "../syndicate", version = "0.30.0"}
|
syndicate = { path = "../syndicate", version = "0.30.0"}
|
||||||
|
|
||||||
clap = { version = "^4.0", features = ["derive"] }
|
clap = { version = "^4.0", features = ["derive"] }
|
||||||
|
|
|
@ -13,11 +13,11 @@ license = "Apache-2.0"
|
||||||
vendored-openssl = ["openssl/vendored"]
|
vendored-openssl = ["openssl/vendored"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
preserves-schema = "4.991"
|
preserves-schema = "4.992"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
preserves = "4.991"
|
preserves = "4.992"
|
||||||
preserves-schema = "4.991"
|
preserves-schema = "4.992"
|
||||||
|
|
||||||
tokio = { version = "1.10", features = ["io-util", "macros", "rt", "rt-multi-thread", "time"] }
|
tokio = { version = "1.10", features = ["io-util", "macros", "rt", "rt-multi-thread", "time"] }
|
||||||
tokio-util = "0.6"
|
tokio-util = "0.6"
|
||||||
|
|
Loading…
Reference in New Issue