use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use ikigai_core::{ArgRef, Error, Iri, Kernel, Request, Verb};
use ikigai_web::{EdgeConfig, HttpRequest, Response};
use tokio::io::AsyncWriteExt;
pub struct Front {
pub kernel: Arc<Kernel>,
pub minter: ikigai_quic::Minter,
pub tokens: PathBuf,
pub edge: EdgeConfig,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Routed {
pub verb: Verb,
pub iri: String,
pub args: Vec<(String, Vec<u8>)>,
pub write_layer: Option<String>,
}
const LD: &str = "application/ld+json";
const JSON: &str = "application/json";
fn refusal(status: u16, kind: &str, message: &str) -> Response {
let body = serde_json::json!({ "kind": kind, "message": message }).to_string();
Response::new(status, JSON, body.into_bytes())
}
fn error_response(e: &Error) -> Response {
Response::new(
ikigai_web::error_status(e),
JSON,
ikigai_web::error_json(e).into_bytes(),
)
}
fn is_layer(name: &str) -> bool {
(1..=24).contains(&name.len())
&& name
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
}
fn is_segment(id: &str) -> bool {
(1..=128).contains(&id.len())
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-' | b'~'))
}
pub fn route(req: &HttpRequest) -> Result<Routed, Response> {
let segments: Vec<&str> = req.path.split('/').filter(|s| !s.is_empty()).collect();
let not_found = || refusal(404, "not-found", &format!("no route for {}", req.path));
let (method, verb, iri, face, write_layer): (&str, Verb, String, Option<&str>, Option<&str>) =
match segments.as_slice() {
["layers", "me"] => (
"GET",
Verb::Source,
"urn:layers:me".into(),
Some(JSON),
None,
),
["layers", "feed"] => (
"GET",
Verb::Source,
"urn:layers:feed".into(),
Some(JSON),
None,
),
["layer", layer, rest @ ..] if is_layer(layer) => {
let at = |local: &str| format!("urn:layer:{layer}:{local}");
match rest {
["context"] => ("GET", Verb::Source, at("context"), None, None),
["entries"] => ("GET", Verb::Source, at("entries"), Some(LD), None),
["entry", id] if is_segment(id) => (
"GET",
Verb::Source,
at(&format!("entry:{id}")),
Some(LD),
None,
),
["graph", slug] if is_segment(slug) => (
"GET",
Verb::Source,
at(&format!("graph:{slug}")),
Some(LD),
None,
),
["latest"] => ("GET", Verb::Source, at("latest"), Some(LD), None),
["feed"] => ("GET", Verb::Source, at("feed"), Some(JSON), None),
["rd"] => {
let one = req.query.iter().any(|(k, _)| k == "tuple");
let face = if one { LD } else { JSON };
("GET", Verb::Source, at("tuples"), Some(face), None)
}
["receipt", id] if is_segment(id) => (
"GET",
Verb::Source,
at(&format!("receipt:{id}")),
Some(LD),
None,
),
["say"] => ("POST", Verb::Sink, at("say"), Some(JSON), Some(*layer)),
["out"] => ("POST", Verb::Sink, at("tuples"), Some(JSON), Some(*layer)),
["take"] => ("POST", Verb::Delete, at("tuples"), Some(LD), None),
["tuples"] if req.method == "GET" => {
let one = req.query.iter().any(|(k, _)| k == "tuple");
let face = if one { LD } else { JSON };
("GET", Verb::Source, at("tuples"), Some(face), None)
}
["tuples"] => match req.header("x-http-method-override") {
None => ("POST", Verb::Sink, at("tuples"), Some(JSON), Some(*layer)),
Some(o) if o.trim().eq_ignore_ascii_case("DELETE") => {
("POST", Verb::Delete, at("tuples"), Some(LD), None)
}
Some(other) => {
return Err(refusal(
400,
"invalid-argument",
&format!(
"X-HTTP-Method-Override: {other} — only DELETE (a take) is \
overridden here"
),
))
}
},
_ => return Err(not_found()),
}
}
_ => return Err(not_found()),
};
if req.method != method {
let allow = if segments.last() == Some(&"tuples") {
"GET, POST"
} else {
method
};
return Err(refusal(
405,
"method-not-allowed",
&format!("{} takes {method} only", req.path),
)
.with_header("Allow", allow));
}
let mut args: Vec<(String, Vec<u8>)> = req
.query
.iter()
.filter(|(k, _)| k != "principal" && k != "content")
.map(|(k, v)| (k.clone(), v.clone().into_bytes()))
.collect();
if let Some(face) = face {
if !args.iter().any(|(k, _)| k == "as") {
args.push(("as".to_string(), face.as_bytes().to_vec()));
}
}
if verb == Verb::Sink {
args.push(("content".to_string(), req.body.clone()));
}
Ok(Routed {
verb,
iri,
args,
write_layer: write_layer.map(str::to_string),
})
}
fn bearer(req: &HttpRequest) -> Option<&str> {
let value = req.header("authorization")?.trim();
let (scheme, token) = value.split_once(char::is_whitespace)?;
(scheme.eq_ignore_ascii_case("bearer") && !token.trim().is_empty()).then(|| token.trim())
}
fn unauthenticated() -> Response {
refusal(
401,
"denied",
"authentication required: send `Authorization: Bearer <token>` with a token the \
operator minted for you (`ikigai layers-token mint`)",
)
.with_header("WWW-Authenticate", "Bearer realm=\"layers\"")
}
impl Front {
fn session(&self, req: &HttpRequest) -> Result<ikigai_quic::Session, Response> {
let Some(token) = bearer(req) else {
return Err(unauthenticated());
};
let tokens = match ikigai_embedded::tokens::load_from(&self.tokens) {
Ok(tokens) => tokens,
Err(why) => {
crate::diagnose(format_args!(
"ikigai: layers https: REFUSED every token: {why}"
));
return Err(unauthenticated());
}
};
let Some(binding) = tokens.binding_for(token) else {
return Err(unauthenticated());
};
let peer = ikigai_quic::PeerIdentity {
segment_id: format!("token-{}", binding.id),
fingerprint: binding.client.clone(),
};
(self.minter)(&peer).ok_or_else(|| {
crate::diagnose(format_args!(
"ikigai: layers https: REFUSED token {} ({}): its client is not enrolled",
binding.id, binding.label
));
unauthenticated()
})
}
fn issue(
&self,
routed: &Routed,
session: &ikigai_quic::Session,
) -> Result<ikigai_core::Representation, Error> {
let iri = Iri::parse(routed.iri.as_str()).map_err(|e| Error::InvalidArgument {
name: "path".into(),
detail: format!("{e:?}"),
})?;
let mut request = Request::new(routed.verb, iri);
for (name, value) in &routed.args {
request = request.with_arg(name.as_str(), ArgRef::Inline(value.clone()));
}
match ikigai_quic::dispatch(&self.kernel, ikigai_wire::Call::Issue(request), session) {
ikigai_wire::Reply::Resolved(repr, _)
| ikigai_wire::Reply::ResolvedTraced(repr, _, _) => Ok(repr),
ikigai_wire::Reply::ErrorTyped(e) => Err(Error::from(e)),
ikigai_wire::Reply::Error(message) => Err(Error::Endpoint(message)),
other => Err(Error::Endpoint(format!("unexpected reply: {other:?}"))),
}
}
fn under_context(&self, layer: &str, body: Vec<u8>, session: &ikigai_quic::Session) -> Vec<u8> {
if body.iter().find(|b| !b.is_ascii_whitespace()) != Some(&b'{') {
return body;
}
let Ok(serde_json::Value::Object(mut object)) = serde_json::from_slice(&body) else {
return body;
};
if object.contains_key("@context") {
return body;
}
let context = Routed {
verb: Verb::Source,
iri: format!("urn:layer:{layer}:context"),
args: Vec::new(),
write_layer: None,
};
let Ok(repr) = self.issue(&context, session) else {
return body;
};
let Ok(serde_json::Value::Object(mut doc)) = serde_json::from_slice(&repr.bytes) else {
return body;
};
let Some(terms) = doc.remove("@context") else {
return body;
};
let mut with = serde_json::Map::new();
with.insert("@context".into(), terms);
with.append(&mut object);
serde_json::Value::Object(with).to_string().into_bytes()
}
fn answer(&self, req: &HttpRequest) -> Response {
let session = match self.session(req) {
Ok(session) => session,
Err(refused) => return refused,
};
let mut routed = match route(req) {
Ok(routed) => routed,
Err(answered) => return answered,
};
if let Some(layer) = routed.write_layer.clone() {
if let Some((_, content)) = routed.args.iter_mut().find(|(k, _)| k == "content") {
*content = self.under_context(&layer, std::mem::take(content), &session);
}
}
match self.issue(&routed, &session) {
Ok(repr) => {
let content_type = repr.repr_type.to_string();
Response::new(200, &content_type, repr.bytes)
}
Err(e) => error_response(&e),
}
.with_header("Cache-Control", "no-store")
.with_header("X-Content-Type-Options", "nosniff")
}
}
pub fn serve(
front: Front,
listener: std::net::TcpListener,
tls: tokio_rustls::rustls::ServerConfig,
) -> std::io::Result<()> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name("layers-https")
.build()?;
runtime.block_on(async move {
listener.set_nonblocking(true)?;
let listener = tokio::net::TcpListener::from_std(listener)?;
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(tls));
let front = Arc::new(front);
let permits = Arc::new(tokio::sync::Semaphore::new(
front
.edge
.max_connections
.min(tokio::sync::Semaphore::MAX_PERMITS),
));
loop {
let (sock, peer) = ikigai_web::accept_next(&listener).await?;
let permit = Arc::clone(&permits).try_acquire_owned().ok();
let acceptor = acceptor.clone();
let front = Arc::clone(&front);
tokio::spawn(async move {
let admitted = permit.is_some();
let _ = connection(front, acceptor, sock, peer, admitted).await;
drop(permit);
});
}
})
}
async fn connection(
front: Arc<Front>,
acceptor: tokio_rustls::TlsAcceptor,
sock: tokio::net::TcpStream,
peer: SocketAddr,
admitted: bool,
) -> std::io::Result<()> {
let config = front.edge.clone();
let mut tls = match tokio::time::timeout(config.header_timeout, acceptor.accept(sock)).await {
Ok(Ok(tls)) => tls,
_ => return Ok(()),
};
if !admitted {
let busy =
refusal(503, "unavailable", "too many connections").with_header("Retry-After", "1");
return ikigai_web::write_response(&mut tls, busy, &config).await;
}
let Some(req) = ikigai_web::read_request(&mut tls, Some(peer.ip()), &config).await? else {
return Ok(());
};
let answer = tokio::task::spawn_blocking(move || front.answer(&req))
.await
.unwrap_or_else(|_| refusal(500, "endpoint", "the front failed answering this request"));
ikigai_web::write_response(&mut tls, answer, &config).await?;
tls.shutdown().await
}
#[cfg(test)]
mod tests {
use super::*;
fn req(method: &str, path: &str, query: &[(&str, &str)], body: &[u8]) -> HttpRequest {
HttpRequest {
method: method.to_string(),
path: path.to_string(),
query: query
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
headers: Vec::new(),
body: body.to_vec(),
peer: None,
}
}
fn arg<'a>(routed: &'a Routed, name: &str) -> Option<&'a str> {
routed
.args
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| std::str::from_utf8(v).unwrap())
}
#[test]
fn each_participant_verb_routes_to_its_resource_with_its_json_face() {
let cases = [
(
"GET",
"/layers/me",
Verb::Source,
"urn:layers:me",
Some(JSON),
),
(
"GET",
"/layers/feed",
Verb::Source,
"urn:layers:feed",
Some(JSON),
),
(
"GET",
"/layer/talk/context",
Verb::Source,
"urn:layer:talk:context",
None,
),
(
"GET",
"/layer/talk/entries",
Verb::Source,
"urn:layer:talk:entries",
Some(LD),
),
(
"GET",
"/layer/talk/entry/01m4",
Verb::Source,
"urn:layer:talk:entry:01m4",
Some(LD),
),
(
"GET",
"/layer/talk/graph/bob",
Verb::Source,
"urn:layer:talk:graph:bob",
Some(LD),
),
(
"GET",
"/layer/talk/latest",
Verb::Source,
"urn:layer:talk:latest",
Some(LD),
),
(
"GET",
"/layer/talk/feed",
Verb::Source,
"urn:layer:talk:feed",
Some(JSON),
),
(
"GET",
"/layer/tasks/rd",
Verb::Source,
"urn:layer:tasks:tuples",
Some(JSON),
),
(
"GET",
"/layer/tasks/receipt/9z",
Verb::Source,
"urn:layer:tasks:receipt:9z",
Some(LD),
),
(
"POST",
"/layer/talk/say",
Verb::Sink,
"urn:layer:talk:say",
Some(JSON),
),
(
"POST",
"/layer/tasks/out",
Verb::Sink,
"urn:layer:tasks:tuples",
Some(JSON),
),
(
"POST",
"/layer/tasks/take",
Verb::Delete,
"urn:layer:tasks:tuples",
Some(LD),
),
];
for (method, path, verb, iri, face) in cases {
let routed = route(&req(method, path, &[], b"body")).unwrap();
assert_eq!((routed.verb, routed.iri.as_str()), (verb, iri), "{path}");
assert_eq!(arg(&routed, "as"), face, "{path}");
assert_eq!(
arg(&routed, "content").is_some(),
verb == Verb::Sink,
"{path}"
);
}
let say = route(&req("POST", "/layer/talk/say", &[], b"{}")).unwrap();
assert_eq!(say.write_layer.as_deref(), Some("talk"));
let take = route(&req("POST", "/layer/tasks/take", &[], b"")).unwrap();
assert_eq!(take.write_layer, None);
}
#[test]
fn one_tuple_is_a_graph_and_the_callers_as_wins() {
let one = route(&req(
"GET",
"/layer/tasks/rd",
&[("tuple", "urn:layer:tasks:o:1")],
b"",
))
.unwrap();
assert_eq!(arg(&one, "as"), Some(LD));
assert_eq!(arg(&one, "tuple"), Some("urn:layer:tasks:o:1"));
let turtle = route(&req(
"GET",
"/layer/talk/entries",
&[("as", "text/turtle")],
b"",
))
.unwrap();
assert_eq!(arg(&turtle, "as"), Some("text/turtle"));
assert_eq!(turtle.args.iter().filter(|(k, _)| k == "as").count(), 1);
}
#[test]
fn a_caller_cannot_name_its_principal_or_smuggle_content() {
let routed = route(&req(
"GET",
"/layer/talk/entries",
&[
("principal", "urn:collab:participant:mallory"),
("content", "x"),
("after", "3"),
],
b"",
))
.unwrap();
assert_eq!(arg(&routed, "principal"), None);
assert_eq!(arg(&routed, "content"), None);
assert_eq!(arg(&routed, "after"), Some("3"));
let say = route(&req(
"POST",
"/layer/talk/say",
&[("content", "query")],
b"body",
))
.unwrap();
assert_eq!(arg(&say, "content"), Some("body"));
}
#[test]
fn an_unknown_route_is_404_and_a_wrong_method_is_405() {
for path in [
"/",
"/layers",
"/layer/talk",
"/layer/talk/nope",
"/layer/Talk/entries",
"/layer/a-name-longer-than-twenty-four/entries",
"/layer/talk/entry/a%2Fb",
"/layer/talk/entry/a:b",
"/other/talk/entries",
] {
let refused = route(&req("GET", path, &[], b"")).unwrap_err();
assert_eq!(refused.status, 404, "{path}");
let body: serde_json::Value = serde_json::from_slice(&refused.body).unwrap();
assert_eq!(body["kind"], "not-found", "{path}");
}
for (method, path, allow) in [
("GET", "/layer/talk/say", "POST"),
("DELETE", "/layer/tasks/take", "POST"),
("POST", "/layer/talk/entries", "GET"),
("PUT", "/layers/me", "GET"),
] {
let refused = route(&req(method, path, &[], b"")).unwrap_err();
assert_eq!(refused.status, 405, "{method} {path}");
assert!(refused
.headers
.contains(&("Allow".to_string(), allow.to_string())));
}
}
#[test]
fn the_tuples_resource_answers_by_its_own_name_too() {
let rd = route(&req("GET", "/layer/tasks/tuples", &[("after", "2")], b"")).unwrap();
assert_eq!((rd.verb, arg(&rd, "as")), (Verb::Source, Some(JSON)));
let out = route(&req("POST", "/layer/tasks/tuples", &[], b"{}")).unwrap();
assert_eq!(
(out.verb, out.write_layer.as_deref()),
(Verb::Sink, Some("tasks"))
);
let mut take = req(
"POST",
"/layer/tasks/tuples",
&[("tuple", "urn:layer:tasks:o")],
b"",
);
take.headers = vec![("x-http-method-override".into(), "delete".into())];
let take = route(&take).unwrap();
assert_eq!((take.verb, arg(&take, "as")), (Verb::Delete, Some(LD)));
assert_eq!(arg(&take, "content"), None);
let mut odd = req("POST", "/layer/tasks/tuples", &[], b"");
odd.headers = vec![("x-http-method-override".into(), "PUT".into())];
assert_eq!(route(&odd).unwrap_err().status, 400);
let put = route(&req("PUT", "/layer/tasks/tuples", &[], b"")).unwrap_err();
assert_eq!(put.status, 405);
assert!(put
.headers
.contains(&("Allow".to_string(), "GET, POST".to_string())));
}
#[test]
fn only_a_bearer_header_presents_a_token() {
let mut with = req("GET", "/layers/me", &[], b"");
with.headers = vec![("authorization".into(), "Bearer iklt_abc".into())];
assert_eq!(bearer(&with), Some("iklt_abc"));
with.headers = vec![("authorization".into(), "bearer iklt_abc ".into())];
assert_eq!(bearer(&with), Some("iklt_abc"));
for value in ["Basic dXNlcjpwYXNz", "Bearer", "Bearer ", "iklt_abc"] {
with.headers = vec![("authorization".into(), value.into())];
assert_eq!(bearer(&with), None, "{value}");
}
assert_eq!(bearer(&req("GET", "/layers/me", &[], b"")), None);
}
}