use a7n_proto::v1::envelope::Body;
use a7n_proto::v1::{Capability, Envelope, HandshakeError, Hello, PeerInfo, Welcome};
use a7n_proto::{FrameKind, PROTOCOL_VERSION, encode_frame, limits};
use quinn::{Connection, RecvStream, SendStream};
use crate::link::{Link, LinkError, Role, peer_machine_id};
fn check_version(
local: u32,
remote: u32,
local_name: &str,
remote_name: &str,
) -> Result<(), LinkError> {
if local == remote {
return Ok(());
}
let (name, version) = if local < remote {
(local_name, remote)
} else {
(remote_name, local)
};
Err(LinkError::UpdateRequired {
name: name.to_owned(),
version,
})
}
pub(crate) fn common_capabilities(local: &[i32], remote: &[i32]) -> Vec<Capability> {
local
.iter()
.filter(|value| remote.contains(value))
.filter_map(|&value| Capability::try_from(value).ok())
.filter(|&capability| capability != Capability::Unspecified)
.collect()
}
pub(crate) async fn send_envelope(
send: &mut SendStream,
buf: &mut Vec<u8>,
body: Body,
) -> Result<(), LinkError> {
buf.clear();
encode_frame(&Envelope { body: Some(body) }, buf);
send.write_all(buf).await?;
Ok(())
}
pub(crate) async fn read_frame(
recv: &mut RecvStream,
buf: &mut Vec<u8>,
kind: FrameKind,
) -> Result<Envelope, LinkError> {
let mut envelope: Envelope = crate::frame::read(
recv,
buf,
kind,
crate::frame::ChunkBound::Fixed(usize::MAX),
Some(limits::FRAME_READ_TIMEOUT),
)
.await?
.ok_or(LinkError::StreamClosed)?;
let info = match &mut envelope.body {
Some(Body::Hello(hello)) => hello.info.as_mut(),
Some(Body::Welcome(welcome)) => welcome.info.as_mut(),
_ => None,
};
if let Some(info) = info {
info.discard_invalid_monitors();
}
Ok(envelope)
}
fn into_link(
connection: Connection,
streams: (SendStream, RecvStream),
buf: Vec<u8>,
info: PeerInfo,
local: &PeerInfo,
role: Role,
) -> Result<Link, LinkError> {
Ok(Link {
peer_id: peer_machine_id(&connection)?,
capabilities: common_capabilities(&local.capabilities, &info.capabilities),
connection,
control_send: streams.0,
control_recv: streams.1,
control_buf: buf,
peer_info: info,
role,
})
}
pub(crate) fn local_info(
connection: &Connection,
endpoint: &quinn::Endpoint,
local: &PeerInfo,
) -> PeerInfo {
let source = connection
.local_ip()
.or_else(|| {
endpoint
.local_addr()
.ok()
.map(|addr| addr.ip())
.filter(|ip| !ip.is_unspecified())
})
.or_else(|| a7n_platform::local_addr_for(connection.remote_address()));
let mut info = local.clone();
info.hw_addr = source
.and_then(a7n_platform::hw_addr_for)
.map_or_else(Vec::new, |mac| mac.to_vec());
info
}
pub(crate) async fn initiate(connection: Connection, local: &PeerInfo) -> Result<Link, LinkError> {
let (mut send, mut recv) = connection.open_bi().await?;
let hello = Hello {
protocol_version: PROTOCOL_VERSION,
info: Some(local.clone()),
};
send_envelope(&mut send, &mut Vec::new(), Body::Hello(hello)).await?;
let mut buf = Vec::new();
let Some(Body::Welcome(welcome)) = read_frame(&mut recv, &mut buf, FrameKind::Control)
.await?
.body
else {
return Err(LinkError::InvalidHello);
};
let info = welcome.info.clone().unwrap_or_default();
check_version(
PROTOCOL_VERSION,
welcome.protocol_version,
&local.name,
&info.name,
)?;
if welcome.error() != HandshakeError::None {
return Err(LinkError::InvalidHello);
}
into_link(connection, (send, recv), buf, info, local, Role::Initiator)
}
pub(crate) async fn respond(
connection: Connection,
local: &PeerInfo,
accepts_drive: bool,
) -> Result<Link, LinkError> {
let (mut send, mut recv) = connection.accept_bi().await?;
let mut buf = Vec::new();
let first = read_frame(&mut recv, &mut buf, FrameKind::FirstHello).await?;
let Some(Body::Hello(hello)) = first.body else {
return reject(
&mut send,
local,
HandshakeError::InvalidHello,
LinkError::InvalidHello,
)
.await;
};
let info = hello.info.clone().unwrap_or_default();
if let Err(error) = check_version(
PROTOCOL_VERSION,
hello.protocol_version,
&local.name,
&info.name,
) {
return reject(&mut send, local, HandshakeError::UnsupportedVersion, error).await;
}
let welcome = Welcome {
protocol_version: PROTOCOL_VERSION,
info: Some(local.clone()),
accepts_drive,
error: HandshakeError::None.into(),
};
send_envelope(&mut send, &mut Vec::new(), Body::Welcome(welcome)).await?;
into_link(connection, (send, recv), buf, info, local, Role::Responder)
}
async fn reject(
send: &mut SendStream,
local: &PeerInfo,
code: HandshakeError,
error: LinkError,
) -> Result<Link, LinkError> {
let welcome = Welcome {
protocol_version: PROTOCOL_VERSION,
info: Some(local.clone()),
accepts_drive: false,
error: code.into(),
};
send_envelope(send, &mut Vec::new(), Body::Welcome(welcome)).await?;
let _ = send.finish();
let _ = send.stopped().await;
Err(error)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_version_major_mismatch_names_older_machine_to_update() {
let cases = [
(1, 2, Some("update laptop to protocol v2")),
(2, 1, Some("update desktop to protocol v2")),
(1, 1, None),
];
for (local, remote, expected) in cases {
let result = check_version(local, remote, "laptop", "desktop");
assert_eq!(
result.err().map(|error| error.to_string()).as_deref(),
expected,
"{local} vs {remote}"
);
}
}
#[test]
fn common_capabilities_unknown_remote_capability_is_dropped() {
let unknown = 9999;
let local = [
Capability::ClipboardText as i32,
Capability::Gestures as i32,
];
let remote = [Capability::ClipboardText as i32, unknown];
let common = common_capabilities(&local, &remote);
assert_eq!(common, vec![Capability::ClipboardText]);
}
}