use notochord::{
DenyReason, HandshakeLimits, IoHandshakeError, ProofBinding, SessionHello, SessionReply,
initiate_session,
};
use transport::{PeerID, Transport, TransportError, initiator_binding};
use crate::carrier::projection_alpn;
pub use graphshell_network::{CarrierRuntime, NetworkCarrier};
#[derive(Debug, thiserror::Error)]
pub enum DialError {
#[error("projection carrier could not reach the peer: {0}")]
Connect(#[from] TransportError),
#[error(transparent)]
Handshake(#[from] IoHandshakeError),
}
pub fn projection_binding(local: PeerID) -> ProofBinding {
initiator_binding(&projection_alpn(), local)
}
pub async fn dial_projection_session<T: Transport>(
transport: &T,
peer: PeerID,
hello: &SessionHello,
limits: &HandshakeLimits,
) -> Result<Result<T::Stream, DenyReason>, DialError> {
let mut stream = transport.connect(peer, projection_alpn()).await?;
match initiate_session(&mut stream, hello, limits).await? {
SessionReply::Reject { reason } => Ok(Err(reason)),
SessionReply::Accept { .. } => Ok(Ok(stream)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::admission::{CONNECT_ACTION, GRAPHSHELL_DOMAIN, PROJECTION_SERVICE};
use crate::lifecycle::SessionAuthority;
use crate::session_loop::{SessionEnd, serve_admitted_session};
use chirograph::{
CapabilityProfile, Carrier, CarrierRequestBody, CarrierResponseBody, EndpointDescriptor,
IntentInvocation, IntentResult, ProjectionRequest, ProjectionSnapshot, ProtocolVersion,
ResourceRequest, ResourceResponse, ResumeRequest, SessionOpen,
};
use graphshell_endpoint::{
IntentSink, PresentationSource, ProjectionCatalog, ProjectionSource,
};
use notochord::{
AdmittedPrincipal, AdmittedSession, CarrierKind, NetworkId, ProfileRef, RequestedAction,
RevocationLedger, SessionClaims, SessionFacts, TrafficClass,
};
use personae::delegation::{
CapabilityScope, DelegationCertificate, DelegationParent, SignedDelegationCertificate,
};
use personae::{IdentityProvider, InMemoryProvider};
use std::fmt::Display;
use std::sync::RwLock;
use tokio::io::DuplexStream;
use tokio::runtime::Runtime;
const NOW_MS: u64 = 50;
const EXPIRY_MS: u64 = 100;
const ROOT_AUTHORITY: [u8; 32] = [7; 32];
const NETWORK: [u8; 32] = [3; 32];
fn owner() -> InMemoryProvider {
InMemoryProvider::from_seed([1; 32])
}
fn viewer() -> InMemoryProvider {
InMemoryProvider::from_seed([4; 32])
}
fn grant() -> SignedDelegationCertificate {
SignedDelegationCertificate::issue(
&owner(),
DelegationCertificate::new(
DelegationParent::Root(ROOT_AUTHORITY),
owner().master_public_key().to_bytes(),
viewer().master_public_key().to_bytes(),
CapabilityScope {
domain: GRAPHSHELL_DOMAIN.into(),
resource: NETWORK.to_vec(),
path_prefix: PROJECTION_SERVICE.into(),
actions: [CONNECT_ACTION.to_string()].into_iter().collect(),
},
5,
10,
Some(EXPIRY_MS),
1,
[1; 32],
),
)
.expect("issue certificate")
}
fn principal() -> AdmittedPrincipal {
AdmittedPrincipal {
subject: viewer().master_public_key().to_bytes(),
class: TrafficClass::Interactive,
session_id: [21; 32],
action: RequestedAction {
domain: GRAPHSHELL_DOMAIN.to_string(),
path: PROJECTION_SERVICE.to_string(),
action: CONNECT_ACTION.to_string(),
},
}
}
fn admitted(stream: DuplexStream) -> AdmittedSession<DuplexStream> {
AdmittedSession {
stream,
principal: principal(),
claims: SessionClaims {
wire_version: 1,
network: NetworkId(NETWORK),
profile: ProfileRef {
id: "mere.base".into(),
revision: 1,
},
action: principal().action,
class: TrafficClass::Interactive,
subject: viewer().master_public_key().to_bytes(),
delegations: vec![grant()],
},
facts: SessionFacts::new(b"mere/graphshell/v1", CarrierKind::P2panda),
limits: Default::default(),
}
}
struct StubEndpoint;
#[derive(Debug)]
struct StubError;
impl Display for StubError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "stub endpoint")
}
}
impl ProjectionCatalog for StubEndpoint {
fn describe(&self) -> EndpointDescriptor {
EndpointDescriptor {
label: "stub".into(),
projections: Vec::new(),
}
}
}
impl ProjectionSource for StubEndpoint {
type Error = StubError;
fn snapshot(&mut self, _: ProjectionRequest) -> Result<ProjectionSnapshot, StubError> {
Err(StubError)
}
}
impl PresentationSource for StubEndpoint {
type Error = StubError;
fn resource(&mut self, _: ResourceRequest) -> Result<ResourceResponse, StubError> {
Err(StubError)
}
}
impl IntentSink for StubEndpoint {
type Error = StubError;
fn invoke(&mut self, _: IntentInvocation) -> Result<IntentResult, StubError> {
Err(StubError)
}
}
fn open_body() -> CarrierRequestBody {
CarrierRequestBody::Open(Box::new(SessionOpen {
version: ProtocolVersion { major: 1, minor: 0 },
capabilities: CapabilityProfile::default(),
}))
}
#[test]
fn the_carrier_speaks_to_the_served_loop_it_will_meet_in_production() {
let runtime = Runtime::new().unwrap();
let (client, server) = tokio::io::duplex(64 * 1024);
let served = runtime.spawn(async move {
let mut session = admitted(server);
let mut endpoint = StubEndpoint;
let mut resume = |_: &mut StubEndpoint, _: ResumeRequest| Err("no resume".to_string());
serve_admitted_session(
&mut session,
&SessionAuthority::retain(principal(), vec![grant()]),
&RwLock::new(RevocationLedger::new()),
&mut endpoint,
&mut resume,
|| NOW_MS,
)
.await
.unwrap()
});
let mut carrier =
NetworkCarrier::over(client, CarrierRuntime::borrowed(runtime.handle().clone()));
match carrier.request(open_body()).unwrap() {
CarrierResponseBody::Opened(opened) => assert_eq!(opened.descriptor.label, "stub"),
other => panic!("expected an opened session, got {other:?}"),
}
assert!(matches!(
carrier.request(CarrierRequestBody::Close).unwrap(),
CarrierResponseBody::Closed
));
let summary = runtime.block_on(served).unwrap();
assert_eq!(summary.end, SessionEnd::Closed);
assert_eq!(summary.answered, 2);
}
}