use std::time::Duration;
use serde_json::value::RawValue;
use serde_json::{Value, json};
use crate::client::{Address, Client, Ended, Refusal, Reply};
use crate::serve::pass::now;
#[derive(Debug)]
pub enum Carried {
Answered(Box<RawValue>),
Refused(Refusal),
}
#[derive(Debug, Clone, Copy)]
pub enum Presenting<'a> {
Pass(&'a str),
Local,
}
pub fn carry(
store: &Address,
timeout: Duration,
token: &str,
method: &str,
params: Option<&RawValue>,
) -> Result<Carried, Ended> {
carry_as(store, "", timeout, Presenting::Pass(token), method, params)
}
pub fn carry_as(
store: &Address,
name: &str,
timeout: Duration,
presenting: Presenting<'_>,
method: &str,
params: Option<&RawValue>,
) -> Result<Carried, Ended> {
if method == "session.pass" {
return Ok(Carried::Refused(Refusal {
code: "invalid_operation".to_owned(),
message: "session.pass is the door's own and is never carried".to_owned(),
path: None,
verified: None,
}));
}
let mut client = Client::reach(store, timeout)?;
if method == "hello" {
return Ok(Carried::Answered(client.hello().to_owned()));
}
if let Some(refusal) = mismatch(client.hello(), name, presenting) {
return Ok(Carried::Refused(refusal));
}
if let Presenting::Pass(token) = presenting {
let pass = serde_json::value::to_raw_value(&json!({ "pass": token }))
.map_err(|error| Ended::NotAsked(format!("the pass could not be written: {error}")))?;
if let Reply::Refused(refusal) = client.call("session.pass", Some(&pass))? {
return Ok(Carried::Refused(refusal));
}
}
Ok(match client.call(method, params)? {
Reply::Answered(answer) => Carried::Answered(answer),
Reply::Refused(refusal) => Carried::Refused(refusal),
})
}
pub enum Followed {
Following(Following),
Refused(Refusal),
}
pub struct Following {
pub first: Box<RawValue>,
client: Client,
expires: Option<u64>,
timeout: Duration,
}
impl Following {
pub fn next_frame(&mut self) -> Result<Reply, Ended> {
self.client.next_reply()
}
pub(super) fn disconnect_handle(&self) -> std::io::Result<crate::client::network::Hangup> {
self.client.follow_socket()
}
fn bound_reads(&self) {
let bound = self.expires.map(|expires| {
let left = Duration::from_secs(expires.saturating_sub(now()));
left.saturating_add(self.timeout)
});
if let Err(ended) = self.client.read_within(bound) {
eprintln!("haematite door: a follow's read bound was not set: {ended}");
}
}
}
pub fn follow(
store: &Address,
timeout: Duration,
token: &str,
branch: &str,
) -> Result<Followed, Ended> {
follow_as(store, "", timeout, Presenting::Pass(token), branch)
}
pub fn follow_as(
store: &Address,
name: &str,
timeout: Duration,
presenting: Presenting<'_>,
branch: &str,
) -> Result<Followed, Ended> {
let mut client = Client::reach(store, timeout)?;
if let Some(refusal) = mismatch(client.hello(), name, presenting) {
return Ok(Followed::Refused(refusal));
}
let expires = match presenting {
Presenting::Pass(token) => {
let pass = raw(&json!({ "pass": token }))?;
match client.call("session.pass", Some(&pass))? {
Reply::Refused(refusal) => return Ok(Followed::Refused(refusal)),
Reply::Answered(answer) => expiry(&answer),
}
}
Presenting::Local => None,
};
let params = raw(&json!({ "branch": branch }))?;
Ok(match client.call("head.follow", Some(¶ms))? {
Reply::Answered(first) => {
let following = Following {
first,
client,
expires,
timeout,
};
following.bound_reads();
Followed::Following(following)
}
Reply::Refused(refusal) => Followed::Refused(refusal),
})
}
fn mismatch(hello: &RawValue, name: &str, presenting: Presenting<'_>) -> Option<Refusal> {
let through_lys = serde_json::from_str::<Value>(hello.get())
.ok()?
.get("lys")?
.as_bool()?;
let store = if name.is_empty() {
"this store".to_owned()
} else {
format!("store {name}")
};
let (code, message) = match (presenting, through_lys) {
(Presenting::Local, true) => (
"door_store_needs_lys",
format!(
"{store} is served through Lys, and this door has no lys block, so it signs nobody in through Lys; add a lys block to the door to reach it"
),
),
(Presenting::Pass(_), false) => (
"door_store_without_lys",
format!(
"{store} is not served through Lys, and this door signs people in through Lys and carries their pass; serve the store with a [lys] block, or run the door with no lys block"
),
),
_ => return None,
};
Some(Refusal {
code: code.to_owned(),
message,
path: None,
verified: None,
})
}
fn raw(value: &Value) -> Result<Box<RawValue>, Ended> {
serde_json::value::to_raw_value(value)
.map_err(|error| Ended::NotAsked(format!("the request could not be written: {error}")))
}
fn expiry(answer: &RawValue) -> Option<u64> {
let answer: Value = serde_json::from_str(answer.get()).ok()?;
answer.get("expires")?.as_u64()
}
#[cfg(test)]
#[path = "forward_tests.rs"]
mod tests;