use liminal::protocol::{Frame, LinkOffer, LinkOpenOutcome, MessageEnvelope};
use super::apply::{publish_response, unix_seconds_now};
use super::state::{ConnectionProcessState, FrameAction};
use super::supervisor::ConnectionRuntime;
use crate::auth_lys::session::LysSession;
use crate::auth_lys::{AccessDecisionError, DeliberateDecision, GrantReceipt, SubjectBinding};
use crate::link::{Link, LinkAction, LinkRefusal, LinkRights};
use crate::link_handshake::{LinkHandshake, LinkTransport, NOISE_OVERHEAD};
pub(super) const LINK_CROSSING_REFUSED_CODE: u16 = 0x0203;
#[derive(Debug)]
pub(super) struct OpenLink {
link: Link,
remote_end: LinkAction,
transport: LinkTransport,
}
impl OpenLink {
pub(super) const fn remote_end(&self) -> LinkAction {
self.remote_end
}
}
#[derive(Debug)]
struct SessionLinkRights<'a>(&'a dyn LysSession);
impl LinkRights for SessionLinkRights<'_> {
fn holds(
&self,
action: LinkAction,
subject: &SubjectBinding,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
self.0.decide_link(subject, action, now)
}
}
enum OpenRefusal {
Held { grant: String, draft: String },
Refused(String),
}
fn refusal_text(error: &AccessDecisionError) -> String {
serde_json::to_string(&error.refusal).unwrap_or_else(|_| error.cause.to_string())
}
pub(super) fn link_open_frame_response(
runtime: &ConnectionRuntime,
state: &mut ConnectionProcessState,
stream_id: u32,
offer: &LinkOffer,
) -> FrameAction {
let outcome = match open_link(runtime, state, stream_id, offer) {
Ok((open, handshake)) => {
state.links.insert(stream_id, open);
LinkOpenOutcome::Opened { handshake }
}
Err(OpenRefusal::Held { grant, draft }) => LinkOpenOutcome::Held { grant, draft },
Err(OpenRefusal::Refused(reason)) => LinkOpenOutcome::Refused { reason },
};
FrameAction::Respond(Frame::LinkOpenAck {
flags: 0,
stream_id,
outcome,
})
}
fn open_link(
runtime: &ConnectionRuntime,
state: &ConnectionProcessState,
stream_id: u32,
offer: &LinkOffer,
) -> Result<(OpenLink, Vec<u8>), OpenRefusal> {
let Some(session) = state.lys_session.as_ref() else {
return Err(OpenRefusal::Refused(
"a link is opened on a connection admitted on a Lys pass".to_owned(),
));
};
let Some((_, network_id)) = runtime.lys_admitter() else {
return Err(OpenRefusal::Refused(
"no lys admitter is installed".to_owned(),
));
};
let Some(host) = runtime.link_host() else {
return Err(OpenRefusal::Refused(
"this liminal holds no machine identity; [auth.lys.machine] is not configured"
.to_owned(),
));
};
if state.links.contains_key(&stream_id) {
return Err(OpenRefusal::Refused(format!(
"a link is already open on stream {stream_id}"
)));
}
let Some(recorded) = host.recorded_machine(session.holder()) else {
return Err(OpenRefusal::Refused(
"no machine key is recorded for this holder".to_owned(),
));
};
if recorded.ed25519_public() != &offer.machine {
return Err(OpenRefusal::Refused(
"the offered machine key is not the holder's recorded key".to_owned(),
));
}
let remote_end = LinkAction::from(offer.end);
let local = host.local().machine();
let (exporter, importer) = match remote_end {
LinkAction::Export => (recorded, local),
LinkAction::Import => (local, recorded),
};
let link = Link::new(
network_id.to_owned(),
offer.link_id.clone(),
exporter,
importer,
)
.map_err(|error| OpenRefusal::Refused(error.to_string()))?;
match session.open_link(&link.resource(), remote_end, unix_seconds_now()) {
Ok(DeliberateDecision::Outright(_)) => {}
Ok(
DeliberateDecision::ByDraft { grant, draft }
| DeliberateDecision::ByTwo { grant, draft },
) => return Err(OpenRefusal::Held { grant, draft }),
Err(error) => return Err(OpenRefusal::Refused(refusal_text(&error))),
}
let handshake_refused = |error: crate::link_handshake::LinkHandshakeError| {
OpenRefusal::Refused(format!("handshake refused: {error}"))
};
let mut handshake =
LinkHandshake::responder(host.local(), recorded).map_err(handshake_refused)?;
handshake
.read(&offer.handshake)
.map_err(handshake_refused)?;
let mut answer = vec![0_u8; NOISE_OVERHEAD];
let written = handshake.write(&mut answer).map_err(handshake_refused)?;
answer.truncate(written);
let transport = handshake.finish().map_err(handshake_refused)?;
Ok((
OpenLink {
link,
remote_end,
transport,
},
answer,
))
}
pub(super) fn link_message_frame_response(
pid: u64,
runtime: &ConnectionRuntime,
state: &mut ConnectionProcessState,
stream_id: u32,
subject: &str,
sealed: &[u8],
) -> FrameAction {
match cross(runtime, state, stream_id, subject, sealed) {
Ok(envelope) => publish_response(
pid,
runtime.services(),
state.negotiated_version,
stream_id,
subject,
&envelope,
None,
),
Err(reason) => FrameAction::Respond(Frame::PublishError {
flags: 0,
stream_id,
reason_code: LINK_CROSSING_REFUSED_CODE,
message: Some(reason),
}),
}
}
fn cross(
runtime: &ConnectionRuntime,
state: &mut ConnectionProcessState,
stream_id: u32,
subject: &str,
sealed: &[u8],
) -> Result<MessageEnvelope, String> {
let ConnectionProcessState {
lys_session, links, ..
} = state;
let Some(session) = lys_session.as_ref() else {
return Err("a crossing is decided on a connection admitted on a Lys pass".to_owned());
};
let Some(host) = runtime.link_host() else {
return Err("this liminal holds no machine identity".to_owned());
};
let Some(open) = links.get_mut(&stream_id) else {
return Err(format!("no link is open on stream {stream_id}"));
};
if open.remote_end() != LinkAction::Export {
return Err(
"messages cross from the exporting end; the remote machine imports on this link"
.to_owned(),
);
}
let mut plain = vec![0_u8; sealed.len()];
let written = open
.transport
.open(sealed, &mut plain)
.map_err(|error| format!("link frame refused: {error}"))?;
plain.truncate(written);
let remote = SessionLinkRights(session.as_ref());
open.link
.crossing(&remote, host.rights(), subject, unix_seconds_now())
.map_err(|refusal| match &refusal {
LinkRefusal::Export(error) => {
format!("crossing refused at export: {}", refusal_text(error))
}
LinkRefusal::Import(error) => {
format!("crossing refused at import: {}", refusal_text(error))
}
LinkRefusal::Binding(_) => refusal.to_string(),
})?;
MessageEnvelope::from_wire_bytes(&plain)
.map_err(|error| format!("the crossed message is not an envelope: {error}"))
}