diff --git a/s2energy-connection/examples/pairing-server.rs b/s2energy-connection/examples/pairing-server.rs index c81b2f7..1e1399c 100644 --- a/s2energy-connection/examples/pairing-server.rs +++ b/s2energy-connection/examples/pairing-server.rs @@ -1,10 +1,9 @@ use axum_server::tls_rustls::RustlsConfig; -use rustls::pki_types::{CertificateDer, pem::PemObject}; use std::{net::SocketAddr, path::PathBuf, sync::Arc}; use uuid::uuid; use s2energy_connection::{ - MessageVersion, S2NodeDescription, S2Role, + MessageVersion, S2EndpointDescription, S2NodeDescription, S2Role, pairing::{NodeConfig, PairingS2NodeId, PairingToken, Server, ServerConfig}, }; use tracing_subscriber::{EnvFilter, fmt, prelude::*}; @@ -19,7 +18,11 @@ async fn main() { .with(EnvFilter::from_default_env()) .init(); - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let config = NodeConfig::builder( S2NodeDescription { id: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8").into(), diff --git a/s2energy-connection/src/pairing/client.rs b/s2energy-connection/src/pairing/client.rs index d7d72ba..779d5a7 100644 --- a/s2energy-connection/src/pairing/client.rs +++ b/s2energy-connection/src/pairing/client.rs @@ -2,8 +2,9 @@ use std::sync::Arc; use reqwest::{StatusCode, Url}; use rustls::pki_types::CertificateDer; -use tracing::{debug, trace}; +use tracing::{Instrument, Span, debug, span, trace}; +use crate::S2NodeId; use crate::common::negotiate_version; use crate::common::wire::{AccessToken, Deployment, PairingVersion, S2Role}; use crate::pairing::transport::{HashProvider, hash_providing_https_client}; @@ -22,6 +23,15 @@ pub struct PairingRemote { pub id: PairingS2NodeId, } +/// Remote node to pair with. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PrePairingRemote { + /// URL at which the remote node can be reached + pub url: String, + /// S2 node id of the remote node. + pub id: S2NodeId, +} + /// Configuration for pairing clients. pub struct ClientConfig { /// Additional roots of trust for TLS connections. Useful when testing during the development of WAN endpoints. @@ -41,6 +51,42 @@ pub struct Client { pairing_deployment: Deployment, } +pub struct PrePairing<'a> { + span: tracing::Span, + remote_id: S2NodeId, + session: V1Session<'a>, + local_deployment: Deployment, + certhash: Option, +} + +impl PrePairing<'_> { + pub async fn cancel(self) -> PairingResult<()> { + self.session + .client + .post(self.session.base_url.join("cancelPreparePairing").unwrap()) + .json(&CancelPrePairingRequest { + client_id: self.session.config.node_description.id, + server_id: Some(self.remote_id), + }) + .send() + .instrument(self.span) + .await + .map_err(|e| Error::new(ErrorKind::TransportFailed, e))?; + Ok(()) + } + + pub async fn pair(self, remote_id: PairingS2NodeId, pairing_token: &[u8]) -> PairingResult { + async move { + trace!("Start pairing after pre-pairing."); + self.session + .pair(self.certhash, self.local_deployment, remote_id, pairing_token) + .await + } + .instrument(self.span) + .await + } +} + impl Client { /// Create a new client for pairing on an node with the given configuration. pub fn new(config: Arc, client_config: ClientConfig) -> PairingResult { @@ -51,12 +97,54 @@ impl Client { }) } + /// Start a pre-pairing session with the remote. This can be used to trigger the remote to provide the user with a pairing code and such. + pub async fn prepair(&self, remote: PrePairingRemote) -> PairingResult> { + let span = span!(tracing::Level::ERROR, "prepair", local = %self.config.node_description.id, remote = ?remote); + let span_clone = span.clone(); + async move { + trace!("Start pre-pairing with new remote."); + let url = Url::try_from(remote.url.as_str()).map_err(|e| Error::new(ErrorKind::InvalidUrl, e))?; + + let (client, certhash) = self.prepare_reqwest_client(&url)?; + + trace!("Prepared reqwest client."); + + let pairing_version = negotiate_version(&client, url.clone()).await?; + + match pairing_version { + PairingVersion::V1 => { + V1Session::new(client, url, &self.config) + .prepair(certhash, self.pairing_deployment, remote.id, span) + .await + } + } + } + .instrument(span_clone) + .await + } + /// Pair with a given remote S2 node, using the provided token. #[tracing::instrument(skip_all, fields(local = %self.config.node_description.id, remote = ?remote), level = tracing::Level::ERROR)] pub async fn pair(&self, remote: PairingRemote, pairing_token: &[u8]) -> PairingResult { trace!("Start pairing with new remote."); let url = Url::try_from(remote.url.as_str()).map_err(|e| Error::new(ErrorKind::InvalidUrl, e))?; + let (client, certhash) = self.prepare_reqwest_client(&url)?; + + trace!("Prepared reqwest client."); + + let pairing_version = negotiate_version(&client, url.clone()).await?; + + match pairing_version { + PairingVersion::V1 => { + V1Session::new(client, url, &self.config) + .pair(certhash, self.pairing_deployment, remote.id, pairing_token) + .await + } + } + } + + fn prepare_reqwest_client(&self, url: &Url) -> Result<(reqwest::Client, Option), Error> { let (client, certhash) = if url.domain().map(|v| v.ends_with(".local")).unwrap_or_default() { let (client, certhash) = hash_providing_https_client()?; (client, Some(certhash)) @@ -73,18 +161,7 @@ impl Client { None, ) }; - - trace!("Prepared reqwest client."); - - let pairing_version = negotiate_version(&client, url.clone()).await?; - - match pairing_version { - PairingVersion::V1 => { - V1Session::new(client, url, &self.config) - .pair(certhash, self.pairing_deployment, remote.id, pairing_token) - .await - } - } + Ok((client, certhash)) } } @@ -103,6 +180,53 @@ impl<'a> V1Session<'a> { } } + async fn prepair( + self, + certhash: Option, + local_deployment: Deployment, + id: S2NodeId, + span: Span, + ) -> PairingResult> { + let response = self + .client + .post(self.base_url.join("preparePairing").unwrap()) + .json(&PrePairingRequest { + client_s2_endpoint_description: self.config.endpoint_description.clone(), + client_s2_node_description: self.config.node_description.clone(), + server_id: Some(id), + }) + .send() + .await + .map_err(|e| Error::new(ErrorKind::TransportFailed, e))?; + + if response.status() == StatusCode::BAD_REQUEST { + let response_error: PairingResponseErrorMessage = response.json().await.map_err(|e| Error::new(ErrorKind::ProtocolError, e))?; + return Err(Error::new( + match response_error { + PairingResponseErrorMessage::InvalidCombinationOfRoles => ErrorKind::RemoteOfSameType, + PairingResponseErrorMessage::IncompatibleS2MessageVersions + | PairingResponseErrorMessage::IncompatibleHMACHashingAlgorithms + | PairingResponseErrorMessage::IncompatibleCommunicationProtocols => ErrorKind::NoSupportedVersion, + PairingResponseErrorMessage::S2NodeNotFound | PairingResponseErrorMessage::S2NodeNotProvided => ErrorKind::UnknownNode, + PairingResponseErrorMessage::InvalidPairingToken => ErrorKind::InvalidToken, + PairingResponseErrorMessage::ParsingError | PairingResponseErrorMessage::Other => ErrorKind::ProtocolError, + }, + response_error, + )); + } + if response.status() != StatusCode::NO_CONTENT { + return Err(ErrorKind::ProtocolError.into()); + } + + Ok(PrePairing { + span, + remote_id: id, + session: self, + local_deployment, + certhash, + }) + } + async fn pair( self, certhash: Option, @@ -365,10 +489,12 @@ mod tests { }; use crate::{ - Deployment, MessageVersion, S2EndpointDescription, S2Role, + Deployment, MessageVersion, S2EndpointDescription, S2NodeDescription, S2NodeId, S2Role, common::wire::test::{UUID_A, UUID_B, basic_node_description, pairing_s2_node_id}, pairing::{ - Client, ClientConfig, ErrorKind, Network, NodeConfig, Pairing, PairingRemote, PairingRole, PairingToken, Server, ServerConfig, + Client, ClientConfig, ErrorKind, Network, NodeConfig, NoopPrePairingHandler, Pairing, PairingRemote, PairingRole, PairingToken, + PrePairingHandler, PrePairingResponse, Server, ServerConfig, + client::PrePairingRemote, wire::{ HmacChallenge, HmacChallengeResponse, PairingAttemptId, PairingResponseErrorMessage, RequestPairing, RequestPairingResponse, }, @@ -381,8 +507,19 @@ mod tests { use rustls::pki_types::{CertificateDer, pem::PemObject}; use tokio::task::JoinHandle; - async fn setup_server(config: NodeConfig, overrides: Router<()>) -> (Handle, JoinHandle) { - let server = Server::new(ServerConfig { root_certificate: None }); + async fn setup_server_with_prepairing( + config: NodeConfig, + handler: impl PrePairingHandler, + overrides: Router<()>, + ) -> (Handle, JoinHandle) { + let server = Server::new_with_prepairing( + ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }, + handler, + ); let rustls_config = RustlsConfig::from_pem( include_bytes!("../../testdata/localhost.chain.pem").into(), include_bytes!("../../testdata/localhost.key").into(), @@ -411,6 +548,10 @@ mod tests { (https_server_handle, server_pair_handle) } + async fn setup_server(config: NodeConfig, overrides: Router<()>) -> (Handle, JoinHandle) { + setup_server_with_prepairing(config, NoopPrePairingHandler, overrides).await + } + #[tokio::test] async fn pairing_ok_rm_initiates() { let server_config = NodeConfig::builder(basic_node_description(UUID_A, S2Role::Cem), vec![MessageVersion("v1".into())]) @@ -487,6 +628,179 @@ mod tests { server_handle.shutdown(); } + #[derive(Debug, Clone)] + struct TestPrePairingHandler { + endpoint: Arc>>, + node: Arc>>, + client_id: Arc>>, + target_node: Arc>>>, + response: PrePairingResponse, + } + + impl TestPrePairingHandler { + fn new(response: PrePairingResponse) -> Self { + Self { + endpoint: Arc::default(), + node: Arc::default(), + client_id: Arc::default(), + target_node: Arc::default(), + response, + } + } + } + + impl PrePairingHandler for TestPrePairingHandler { + fn prepairing_requested( + &self, + endpoint: S2EndpointDescription, + node: S2NodeDescription, + target_node: Option, + ) -> PrePairingResponse { + *self.endpoint.lock().unwrap() = Some(endpoint); + *self.node.lock().unwrap() = Some(node); + *self.target_node.lock().unwrap() = Some(target_node); + self.response + } + + fn prepairing_cancelled(&self, id: crate::S2NodeId, target_node: Option) { + *self.client_id.lock().unwrap() = Some(id); + *self.target_node.lock().unwrap() = Some(target_node); + } + } + + #[tokio::test] + async fn prepairing_then_pair() { + let server_config = NodeConfig::builder(basic_node_description(UUID_A, S2Role::Rm), vec![MessageVersion("v1".into())]) + .with_connection_initiate_url("test.example.com".into()) + .build() + .unwrap(); + + let client_config = NodeConfig::builder(basic_node_description(UUID_B, S2Role::Cem), vec![MessageVersion("v1".into())]) + .with_connection_initiate_url("client.example.com".into()) + .build() + .unwrap(); + + let test_handler = TestPrePairingHandler::new(PrePairingResponse::Accept); + let (server_handle, server_pairing) = setup_server_with_prepairing(server_config, test_handler.clone(), Router::new()).await; + + let addr = server_handle.listening().await.unwrap(); + let remote = PrePairingRemote { + url: format!("https://localhost:{}/", addr.port()), + id: UUID_A.into(), + }; + + let client = Client::new( + Arc::new(client_config), + ClientConfig { + additional_certificates: vec![CertificateDer::from_pem_slice(include_bytes!("../../testdata/root.pem")).unwrap()], + pairing_deployment: Deployment::Wan, + }, + ) + .unwrap(); + + let client_prepair = client.prepair(remote).await.unwrap(); + let client_pairing = client_prepair.pair(pairing_s2_node_id(), b"testtoken").await.unwrap(); + let server_pairing = server_pairing.await.unwrap(); + assert_eq!(client_pairing.token, server_pairing.token); + assert_ne!(client_pairing.role, server_pairing.role); + assert!(matches!(client_pairing.role, PairingRole::CommunicationServer)); + + let endpoint = test_handler.endpoint.lock().unwrap().take().unwrap(); + let node = test_handler.node.lock().unwrap().take().unwrap(); + let target_node = test_handler.target_node.lock().unwrap().take().unwrap(); + assert_eq!(endpoint.deployment, None); + assert_eq!(node.id, UUID_B.into()); + assert_eq!(target_node, Some(UUID_A.into())); + + server_handle.shutdown(); + } + + #[tokio::test] + async fn prepairing_then_cancel() { + let server_config = NodeConfig::builder(basic_node_description(UUID_A, S2Role::Rm), vec![MessageVersion("v1".into())]) + .with_connection_initiate_url("test.example.com".into()) + .build() + .unwrap(); + + let client_config = NodeConfig::builder(basic_node_description(UUID_B, S2Role::Cem), vec![MessageVersion("v1".into())]) + .with_connection_initiate_url("client.example.com".into()) + .build() + .unwrap(); + + let test_handler = TestPrePairingHandler::new(PrePairingResponse::Accept); + let (server_handle, _) = setup_server_with_prepairing(server_config, test_handler.clone(), Router::new()).await; + + let addr = server_handle.listening().await.unwrap(); + let remote = PrePairingRemote { + url: format!("https://localhost:{}/", addr.port()), + id: UUID_A.into(), + }; + + let client = Client::new( + Arc::new(client_config), + ClientConfig { + additional_certificates: vec![CertificateDer::from_pem_slice(include_bytes!("../../testdata/root.pem")).unwrap()], + pairing_deployment: Deployment::Wan, + }, + ) + .unwrap(); + + let client_prepair = client.prepair(remote).await.unwrap(); + + let endpoint = test_handler.endpoint.lock().unwrap().take().unwrap(); + let node = test_handler.node.lock().unwrap().take().unwrap(); + let target_node = test_handler.target_node.lock().unwrap().take().unwrap(); + assert_eq!(endpoint.deployment, None); + assert_eq!(node.id, UUID_B.into()); + assert_eq!(target_node, Some(UUID_A.into())); + + client_prepair.cancel().await.unwrap(); + + let client_id = test_handler.client_id.lock().unwrap().take().unwrap(); + let target_node = test_handler.target_node.lock().unwrap().take().unwrap(); + assert_eq!(client_id, UUID_B.into()); + assert_eq!(target_node, Some(UUID_A.into())); + + server_handle.shutdown(); + } + + #[tokio::test] + async fn prepairing_rejected() { + let server_config = NodeConfig::builder(basic_node_description(UUID_A, S2Role::Rm), vec![MessageVersion("v1".into())]) + .with_connection_initiate_url("test.example.com".into()) + .build() + .unwrap(); + + let client_config = NodeConfig::builder(basic_node_description(UUID_B, S2Role::Cem), vec![MessageVersion("v1".into())]) + .with_connection_initiate_url("client.example.com".into()) + .build() + .unwrap(); + + let test_handler = TestPrePairingHandler::new(PrePairingResponse::RejectUnwantedRole); + let (server_handle, _) = setup_server_with_prepairing(server_config, test_handler.clone(), Router::new()).await; + + let addr = server_handle.listening().await.unwrap(); + let remote = PrePairingRemote { + url: format!("https://localhost:{}/", addr.port()), + id: UUID_A.into(), + }; + + let client = Client::new( + Arc::new(client_config), + ClientConfig { + additional_certificates: vec![CertificateDer::from_pem_slice(include_bytes!("../../testdata/root.pem")).unwrap()], + pairing_deployment: Deployment::Wan, + }, + ) + .unwrap(); + + let Err(client_prepair) = client.prepair(remote).await else { + panic!("Unexpected successfull prepairing"); + }; + + assert_eq!(client_prepair.kind(), ErrorKind::RemoteOfSameType) + } + #[tokio::test] async fn pairing_rejects_invalid_hmac() { let server_config = NodeConfig::builder(basic_node_description(UUID_A, S2Role::Cem), vec![MessageVersion("v1".into())]) diff --git a/s2energy-connection/src/pairing/mod.rs b/s2energy-connection/src/pairing/mod.rs index 3b6d5fa..95fdfea 100644 --- a/s2energy-connection/src/pairing/mod.rs +++ b/s2energy-connection/src/pairing/mod.rs @@ -79,6 +79,7 @@ //! # use std::{path::PathBuf, net::SocketAddr}; //! # use axum_server::tls_rustls::RustlsConfig; //! # use s2energy_connection::pairing::{Server, ServerConfig}; +//! # use s2energy_connection::S2EndpointDescription; //! # #[tokio::main(flavor = "current_thread")] //! # async fn main() { //! # let tls_config = RustlsConfig::from_pem_file( @@ -92,6 +93,8 @@ //! # let addr = SocketAddr::from(([127, 0, 0, 1], 8005)); //! let server = Server::new(ServerConfig { //! root_certificate: None, +//! advertised_endpoint: S2EndpointDescription::default(), +//! advertised_nodes: vec![], //! }); //! tokio::spawn(async move { //! axum_server::bind_rustls(addr, tls_config) @@ -106,7 +109,7 @@ //! # use std::{path::PathBuf, net::SocketAddr, sync::Arc}; //! # use axum_server::tls_rustls::RustlsConfig; //! # use s2energy_connection::pairing::{NodeConfig, PairingToken, Server, ServerConfig, PairingS2NodeId}; -//! # use s2energy_connection::{MessageVersion, S2NodeDescription, S2NodeId, S2Role}; +//! # use s2energy_connection::{MessageVersion, S2NodeDescription, S2EndpointDescription, S2NodeId, S2Role}; //! # #[tokio::main(flavor = "current_thread")] //! # async fn main() { //! # let tls_config = RustlsConfig::from_pem_file( @@ -120,6 +123,8 @@ //! # let addr = SocketAddr::from(([127, 0, 0, 1], 8005)); //! # let server = Server::new(ServerConfig { //! # root_certificate: None, +//! # advertised_endpoint: S2EndpointDescription::default(), +//! # advertised_nodes: vec![], //! # }); //! # let config = Arc::new(NodeConfig::builder(S2NodeDescription { //! # id: S2NodeId::new(), @@ -142,7 +147,7 @@ //! # use std::{path::PathBuf, net::SocketAddr, sync::Arc}; //! # use axum_server::tls_rustls::RustlsConfig; //! # use s2energy_connection::pairing::{NodeConfig, PairingToken, Server, ServerConfig, PairingS2NodeId}; -//! # use s2energy_connection::{MessageVersion, S2NodeDescription, S2NodeId, S2Role}; +//! # use s2energy_connection::{MessageVersion, S2NodeDescription, S2EndpointDescription, S2NodeId, S2Role}; //! # #[tokio::main(flavor = "current_thread")] //! # async fn main() { //! # let tls_config = RustlsConfig::from_pem_file( @@ -156,6 +161,8 @@ //! # let addr = SocketAddr::from(([127, 0, 0, 1], 8005)); //! # let server = Server::new(ServerConfig { //! # root_certificate: None, +//! # advertised_endpoint: S2EndpointDescription::default(), +//! # advertised_nodes: vec![], //! # }); //! # let config = Arc::new(NodeConfig::builder(S2NodeDescription { //! # id: S2NodeId::new(), @@ -193,7 +200,9 @@ use wire::{HmacChallenge, HmacChallengeResponse}; pub use client::{Client, ClientConfig, PairingRemote}; pub use error::{ConfigError, Error, ErrorKind}; -pub use server::{PairingToken, PendingPairing, RepeatedPairing, Server, ServerConfig}; +pub use server::{ + NoopPrePairingHandler, PairingToken, PendingPairing, PrePairingHandler, PrePairingResponse, RepeatedPairing, Server, ServerConfig, +}; pub use wire::PairingS2NodeId; use crate::{ @@ -372,4 +381,8 @@ impl Network { Network::Lan { .. } => Deployment::Lan, } } + + fn is_lan(&self) -> bool { + matches!(self, Network::Lan { .. }) + } } diff --git a/s2energy-connection/src/pairing/server.rs b/s2energy-connection/src/pairing/server.rs index 07f0637..3562fe0 100644 --- a/s2energy-connection/src/pairing/server.rs +++ b/s2energy-connection/src/pairing/server.rs @@ -37,8 +37,8 @@ pub struct PairingToken(pub Box<[u8]>); /// Server for handling S2 pairing transactions. /// /// Responsible for providing the HTTP endpoints needed for handling -pub struct Server { - state: AppState, +pub struct Server { + state: AppState, } /// Configuration for the S2 pairing server. @@ -46,6 +46,11 @@ pub struct ServerConfig { /// The root certificate of the server, if we are using a self-signed root. /// Presence of this field indicates we are deployed on LAN. pub root_certificate: Option>, + /// Endpoint description of the server + pub advertised_endpoint: S2EndpointDescription, + /// Initial set of nodes to advertise. This is only used if the server + /// is deployed on LAN. + pub advertised_nodes: Vec, } /// A pending one-time pairing transaction. @@ -76,7 +81,61 @@ impl RepeatedPairing { } } -impl Server { +/// Description of what response to +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PrePairingResponse { + /// Indicate willingness to pair + Accept, + /// Indicate unwillingness because we currently don't manage any nodes. + RejectNoS2Node, + /// Indicate unwillingness because we have the same role as the remote. + RejectUnwantedRole, +} + +/// Handler for pre-pairing requests. +/// +/// This allows notification of other components when a prepairing request +/// comes in. The methods of this trait are called during request handling, +/// and should thus return rapidly. +// +// Note: As these methods are called during request handling, we conciously +// make them not async to encourage not doing long-lasting operations. +pub trait PrePairingHandler: Send + Sync + 'static { + /// Handle a request for prepairing, and indicate our willingness. + fn prepairing_requested( + &self, + endpoint: S2EndpointDescription, + node: S2NodeDescription, + target_node: Option, + ) -> PrePairingResponse; + /// Handle a cancel event for prepairing. Note that not every pre-pairing + /// request will result in a cancel or a pairing interaction, so timeouts + /// may be needed. + fn prepairing_cancelled(&self, id: S2NodeId, target_node: Option); +} + +/// A pre-pairing handler that does nothing on receiving a prepairing request. +/// +/// The requests will always indicate willingness to pair to the client. +pub struct NoopPrePairingHandler; + +impl PrePairingHandler for NoopPrePairingHandler { + fn prepairing_requested( + &self, + _endpoint: S2EndpointDescription, + _node: S2NodeDescription, + _target_node: Option, + ) -> PrePairingResponse { + // no reason not to accept + PrePairingResponse::Accept + } + + fn prepairing_cancelled(&self, _id: S2NodeId, _target_node: Option) { + // noop + } +} + +impl Server { /// Create a new server using the given configuration. pub fn new(server_config: ServerConfig) -> Self { let state = AppStateInner { @@ -86,14 +145,42 @@ impl Server { fingerprint: sha2::Sha256::digest(v).into(), }) .unwrap_or(Network::Wan), + advertised_nodes: Mutex::new(server_config.advertised_nodes), + advertised_endpoint: server_config.advertised_endpoint, + permanent_pairings: Mutex::new(HashMap::new()), + open_pairings: Mutex::new(HashMap::new()), + attempts: Mutex::new(HashMap::default()), + prepairing_handler: Arc::new(NoopPrePairingHandler), + }; + + Self { state: Arc::new(state) } + } +} + +impl Server { + /// Create a new server using the given configuration, with a prepairing + /// handler. + pub fn new_with_prepairing(server_config: ServerConfig, handler: H) -> Self { + let state = AppStateInner { + network: server_config + .root_certificate + .map(|v| Network::Lan { + fingerprint: sha2::Sha256::digest(v).into(), + }) + .unwrap_or(Network::Wan), + advertised_nodes: Mutex::new(server_config.advertised_nodes), + advertised_endpoint: server_config.advertised_endpoint, permanent_pairings: Mutex::new(HashMap::new()), open_pairings: Mutex::new(HashMap::new()), attempts: Mutex::new(HashMap::default()), + prepairing_handler: Arc::new(handler), }; Self { state: Arc::new(state) } } +} +impl Server { /// Get an [`axum::Router`] handling the endpoints for the pairing protocol. /// /// Incomming http requests can be handled by this router through the [axum-server](https://docs.rs/axum-server/0.8.0/axum_server/) crate. @@ -104,6 +191,13 @@ impl Server { .with_state(self.state.clone()) } + /// Update the nodes advertised by this server. + /// + /// These are only used when the server is on a LAN. + pub fn update_advertised_nodes(&self, advertised_nodes: Vec) { + *self.state.advertised_nodes.lock().unwrap() = advertised_nodes; + } + /// Start a one-time pairing session for the given node using the given token. pub fn pair_once( &self, @@ -252,18 +346,24 @@ impl ExpiringPairingState { } } -type AppState = Arc; +type AppState = Arc>; -struct AppStateInner { - // rng: ThreadRng, +struct AppStateInner { network: Network, + advertised_endpoint: S2EndpointDescription, + advertised_nodes: Mutex>, permanent_pairings: Mutex>, open_pairings: Mutex>, attempts: Mutex>, + prepairing_handler: Arc, } -fn v1_router() -> Router { +fn v1_router() -> Router> { Router::new() + .route("/s2endpoint", get(v1_s2endpoint)) + .route("/s2nodes", get(v1_s2nodes)) + .route("/preparePairing", post(v1_prepare_pairing)) + .route("/cancelPreparePairing", post(v1_cancel_prepare_pairing)) .route("/requestPairing", post(v1_request_pairing)) .route("/requestConnectionDetails", post(v1_request_connection_details)) .route("/postConnectionDetails", post(v1_post_connection_details)) @@ -271,8 +371,51 @@ fn v1_router() -> Router { } #[tracing::instrument(skip_all, level = tracing::Level::INFO)] -async fn v1_request_pairing( - State(state): State, +async fn v1_s2endpoint(State(state): State>) -> Result, StatusCode> { + if state.network.is_lan() { + Ok(Json(state.advertised_endpoint.clone())) + } else { + Err(StatusCode::UNAUTHORIZED) + } +} + +#[tracing::instrument(skip_all, level = tracing::Level::INFO)] +async fn v1_s2nodes(State(state): State>) -> Result>, StatusCode> { + if state.network.is_lan() { + Ok(Json(state.advertised_nodes.lock().unwrap().clone())) + } else { + Err(StatusCode::UNAUTHORIZED) + } +} + +#[tracing::instrument(skip_all, level = tracing::Level::INFO)] +async fn v1_prepare_pairing( + State(state): State>, + Json(request): Json, +) -> Result { + match state.prepairing_handler.prepairing_requested( + request.client_s2_endpoint_description, + request.client_s2_node_description, + request.server_id, + ) { + PrePairingResponse::Accept => Ok(StatusCode::NO_CONTENT), + PrePairingResponse::RejectNoS2Node => Err(PairingResponseErrorMessage::S2NodeNotFound), + PrePairingResponse::RejectUnwantedRole => Err(PairingResponseErrorMessage::InvalidCombinationOfRoles), + } +} + +#[tracing::instrument(skip_all, level = tracing::Level::INFO)] +async fn v1_cancel_prepare_pairing( + State(state): State>, + Json(request): Json, +) -> StatusCode { + state.prepairing_handler.prepairing_cancelled(request.client_id, request.server_id); + StatusCode::NO_CONTENT +} + +#[tracing::instrument(skip_all, level = tracing::Level::INFO)] +async fn v1_request_pairing( + State(state): State>, Json(request_pairing): Json, ) -> Result, PairingResponseErrorMessage> { trace!("Received pairing request."); @@ -390,8 +533,8 @@ async fn v1_request_pairing( } #[tracing::instrument(skip_all, level = tracing::Level::INFO)] -async fn v1_request_connection_details( - State(app_state): State, +async fn v1_request_connection_details( + State(app_state): State>, pairing_attempt_id: PairingAttemptId, Json(req): Json, ) -> Result, StatusCode> { @@ -461,8 +604,8 @@ async fn v1_request_connection_details( } #[tracing::instrument(skip_all, level = tracing::Level::INFO)] -async fn v1_post_connection_details( - State(app_state): State, +async fn v1_post_connection_details( + State(app_state): State>, pairing_attempt_id: PairingAttemptId, Json(req): Json, ) -> StatusCode { @@ -523,7 +666,11 @@ async fn v1_post_connection_details( } #[tracing::instrument(skip_all, level = tracing::Level::INFO)] -async fn v1_finalize_pairing(State(state): State, pairing_attempt_id: PairingAttemptId, Json(success): Json) -> StatusCode { +async fn v1_finalize_pairing( + State(state): State>, + pairing_attempt_id: PairingAttemptId, + Json(success): Json, +) -> StatusCode { trace!("Received request to finalize pairing session."); let Some(state) = ({ @@ -583,31 +730,38 @@ async fn v1_finalize_pairing(State(state): State, pairing_attempt_id: #[cfg(test)] mod tests { - use std::sync::Arc; + use std::sync::{Arc, Mutex, OnceLock}; use axum::body::Body; use http::StatusCode; use http_body_util::BodyExt; + use rustls::pki_types::{CertificateDer, pem::PemObject}; use tokio::time::Instant; use tower::ServiceExt; use tracing::{Level, span}; use crate::{ - AccessToken, CommunicationProtocol, MessageVersion, S2EndpointDescription, S2NodeDescription, S2Role, + AccessToken, CommunicationProtocol, MessageVersion, S2EndpointDescription, S2NodeDescription, S2NodeId, S2Role, common::wire::test::{UUID_A, UUID_B, basic_node_description, pairing_s2_node_id}, pairing::{ - ErrorKind, Network, NodeConfig, PairingRole, PairingS2NodeId, PairingToken, Server, ServerConfig, + ErrorKind, Network, NodeConfig, PairingRole, PairingS2NodeId, PairingToken, PrePairingHandler, PrePairingResponse, Server, + ServerConfig, server::{CompletePairingState, ExpiringPairingState, InitialPairingState, PairingRequest, PairingState, ResultSender}, wire::{ - ConnectionDetails, HmacChallenge, HmacHashingAlgorithm, PairingAttemptId, PairingResponseErrorMessage, - PostConnectionDetailsRequest, RequestConnectionDetailsRequest, RequestPairing, RequestPairingResponse, + CancelPrePairingRequest, ConnectionDetails, HmacChallenge, HmacHashingAlgorithm, PairingAttemptId, + PairingResponseErrorMessage, PostConnectionDetailsRequest, PrePairingRequest, RequestConnectionDetailsRequest, + RequestPairing, RequestPairingResponse, }, }, }; #[tokio::test] async fn version_negotiation() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let response = server .get_router() @@ -619,9 +773,297 @@ mod tests { assert_eq!(body, b"[\"v1\"]".as_slice()); } + #[tokio::test] + async fn advertised_endpoint() { + let server = Server::new(ServerConfig { + root_certificate: Some(CertificateDer::from_pem_slice(include_bytes!("../../testdata/root.pem")).unwrap()), + advertised_endpoint: S2EndpointDescription { + name: Some("Testendpoint".into()), + logo_uri: None, + deployment: None, + }, + advertised_nodes: vec![basic_node_description(UUID_A, S2Role::Cem)], + }); + + let response = server + .get_router() + .oneshot(http::Request::get("/v1/s2endpoint").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let body_data: S2EndpointDescription = serde_json::from_slice(&body).unwrap(); + assert_eq!(body_data.name.as_deref(), Some("Testendpoint")); + } + + #[tokio::test] + async fn advertised_endpoint_wan() { + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription { + name: Some("Testendpoint".into()), + logo_uri: None, + deployment: None, + }, + advertised_nodes: vec![basic_node_description(UUID_A, S2Role::Cem)], + }); + + let response = server + .get_router() + .oneshot(http::Request::get("/v1/s2endpoint").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn advertised_nodes() { + let server = Server::new(ServerConfig { + root_certificate: Some(CertificateDer::from_pem_slice(include_bytes!("../../testdata/root.pem")).unwrap()), + advertised_endpoint: S2EndpointDescription { + name: Some("Testendpoint".into()), + logo_uri: None, + deployment: None, + }, + advertised_nodes: vec![basic_node_description(UUID_A, S2Role::Cem)], + }); + + let response = server + .get_router() + .oneshot(http::Request::get("/v1/s2nodes").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let body_data: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(body_data, vec![basic_node_description(UUID_A, S2Role::Cem)]); + } + + #[tokio::test] + async fn advertised_nodes_wan() { + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription { + name: Some("Testendpoint".into()), + logo_uri: None, + deployment: None, + }, + advertised_nodes: vec![basic_node_description(UUID_A, S2Role::Cem)], + }); + + let response = server + .get_router() + .oneshot(http::Request::get("/v1/s2nodes").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[derive(Debug, Clone)] + struct TestPrePairingHandler { + endpoint: Arc>>, + node: Arc>>, + client_id: Arc>>, + target_node: Arc>>>, + response: PrePairingResponse, + } + + impl TestPrePairingHandler { + fn new(response: PrePairingResponse) -> Self { + Self { + endpoint: Arc::default(), + node: Arc::default(), + client_id: Arc::default(), + target_node: Arc::default(), + response, + } + } + } + + impl PrePairingHandler for TestPrePairingHandler { + fn prepairing_requested( + &self, + endpoint: S2EndpointDescription, + node: S2NodeDescription, + target_node: Option, + ) -> super::PrePairingResponse { + *self.endpoint.lock().unwrap() = Some(endpoint); + *self.node.lock().unwrap() = Some(node); + *self.target_node.lock().unwrap() = Some(target_node); + self.response + } + + fn prepairing_cancelled(&self, id: crate::S2NodeId, target_node: Option) { + *self.client_id.lock().unwrap() = Some(id); + *self.target_node.lock().unwrap() = Some(target_node); + } + } + + #[tokio::test] + async fn prepairing_accept() { + let test_handler = TestPrePairingHandler::new(PrePairingResponse::Accept); + let server = Server::new_with_prepairing( + ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }, + test_handler.clone(), + ); + let response = server + .get_router() + .oneshot( + http::Request::post("/v1/preparePairing") + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from( + serde_json::to_vec(&PrePairingRequest { + client_s2_endpoint_description: S2EndpointDescription::default(), + client_s2_node_description: basic_node_description(UUID_A, S2Role::Cem), + server_id: None, + }) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::NO_CONTENT); + + let endpoint = test_handler.endpoint.lock().unwrap().take().unwrap(); + let node = test_handler.node.lock().unwrap().take().unwrap(); + let target_node = test_handler.target_node.lock().unwrap().take().unwrap(); + assert_eq!(endpoint.deployment, None); + assert_eq!(node.role, S2Role::Cem); + assert_eq!(node.id, UUID_A.into()); + assert_eq!(target_node, None); + } + + #[tokio::test] + async fn prepairing_reject_no_node() { + let test_handler = TestPrePairingHandler::new(PrePairingResponse::RejectNoS2Node); + let server = Server::new_with_prepairing( + ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }, + test_handler.clone(), + ); + let response = server + .get_router() + .oneshot( + http::Request::post("/v1/preparePairing") + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from( + serde_json::to_vec(&PrePairingRequest { + client_s2_endpoint_description: S2EndpointDescription::default(), + client_s2_node_description: basic_node_description(UUID_A, S2Role::Cem), + server_id: None, + }) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let response_data: PairingResponseErrorMessage = serde_json::from_slice(&body).unwrap(); + assert_eq!(response_data, PairingResponseErrorMessage::S2NodeNotFound); + + let endpoint = test_handler.endpoint.lock().unwrap().take().unwrap(); + let node = test_handler.node.lock().unwrap().take().unwrap(); + let target_node = test_handler.target_node.lock().unwrap().take().unwrap(); + assert_eq!(endpoint.deployment, None); + assert_eq!(node.role, S2Role::Cem); + assert_eq!(node.id, UUID_A.into()); + assert_eq!(target_node, None); + } + + #[tokio::test] + async fn prepairing_reject_role() { + let test_handler = TestPrePairingHandler::new(PrePairingResponse::RejectUnwantedRole); + let server = Server::new_with_prepairing( + ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }, + test_handler.clone(), + ); + let response = server + .get_router() + .oneshot( + http::Request::post("/v1/preparePairing") + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from( + serde_json::to_vec(&PrePairingRequest { + client_s2_endpoint_description: S2EndpointDescription::default(), + client_s2_node_description: basic_node_description(UUID_A, S2Role::Cem), + server_id: None, + }) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let response_data: PairingResponseErrorMessage = serde_json::from_slice(&body).unwrap(); + assert_eq!(response_data, PairingResponseErrorMessage::InvalidCombinationOfRoles); + + let endpoint = test_handler.endpoint.lock().unwrap().take().unwrap(); + let node = test_handler.node.lock().unwrap().take().unwrap(); + let target_node = test_handler.target_node.lock().unwrap().take().unwrap(); + assert_eq!(endpoint.deployment, None); + assert_eq!(node.role, S2Role::Cem); + assert_eq!(node.id, UUID_A.into()); + assert_eq!(target_node, None); + } + + #[tokio::test] + async fn cancel_prepairing() { + let test_handler = TestPrePairingHandler::new(PrePairingResponse::Accept); + let server = Server::new_with_prepairing( + ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }, + test_handler.clone(), + ); + let response = server + .get_router() + .oneshot( + http::Request::post("/v1/cancelPreparePairing") + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from( + serde_json::to_vec(&CancelPrePairingRequest { + client_id: UUID_B.into(), + server_id: None, + }) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::NO_CONTENT); + + let client_id = test_handler.client_id.lock().unwrap().take().unwrap(); + let target_node = test_handler.target_node.lock().unwrap().take().unwrap(); + assert_eq!(client_id, UUID_B.into()); + assert_eq!(target_node, None); + } + #[tokio::test] async fn pair_attempt() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let pairing_waiter = server .pair_once( Arc::new( @@ -668,7 +1110,11 @@ mod tests { #[tokio::test] async fn pair_attempt_no_common_communication() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let pairing_waiter = server .pair_once( Arc::new( @@ -714,7 +1160,11 @@ mod tests { #[tokio::test] async fn pair_attempt_no_common_messages() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let pairing_waiter = server .pair_once( Arc::new( @@ -760,7 +1210,11 @@ mod tests { #[tokio::test] async fn pair_attempt_forced() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let pairing_waiter = server .pair_once( Arc::new( @@ -807,7 +1261,11 @@ mod tests { #[tokio::test] async fn pair_attempt_with_unknown_node() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let response = server .get_router() @@ -840,7 +1298,11 @@ mod tests { #[tokio::test] async fn pair_attempt_same_role() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let pairing_waiter = server .pair_once( Arc::new( @@ -886,7 +1348,11 @@ mod tests { #[tokio::test] async fn request_connection_details() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let mut attempts = server.state.attempts.lock().unwrap(); let (sender, _receiver) = tokio::sync::oneshot::channel(); let challenge = HmacChallenge::new(&mut rand::rng(), 64); @@ -937,7 +1403,11 @@ mod tests { #[tokio::test] async fn request_connection_details_invalid_response() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let mut attempts = server.state.attempts.lock().unwrap(); let (sender, _receiver) = tokio::sync::oneshot::channel(); let challenge = HmacChallenge::new(&mut rand::rng(), 64); @@ -985,7 +1455,11 @@ mod tests { #[tokio::test(start_paused = true)] async fn request_connection_details_too_late() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let mut attempts = server.state.attempts.lock().unwrap(); let (sender, _receiver) = tokio::sync::oneshot::channel(); let challenge = HmacChallenge::new(&mut rand::rng(), 64); @@ -1035,7 +1509,11 @@ mod tests { #[tokio::test] async fn post_connection_details() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let mut attempts = server.state.attempts.lock().unwrap(); let (sender, _receiver) = tokio::sync::oneshot::channel(); let challenge = HmacChallenge::new(&mut rand::rng(), 64); @@ -1087,7 +1565,11 @@ mod tests { #[tokio::test] async fn post_connection_details_invalid_response() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let mut attempts = server.state.attempts.lock().unwrap(); let (sender, _receiver) = tokio::sync::oneshot::channel(); let challenge = HmacChallenge::new(&mut rand::rng(), 64); @@ -1139,7 +1621,11 @@ mod tests { #[tokio::test(start_paused = true)] async fn post_connection_details_too_late() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let mut attempts = server.state.attempts.lock().unwrap(); let (sender, _receiver) = tokio::sync::oneshot::channel(); let challenge = HmacChallenge::new(&mut rand::rng(), 64); @@ -1193,7 +1679,11 @@ mod tests { #[tokio::test] async fn finalize() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let mut attempts = server.state.attempts.lock().unwrap(); let (sender, receiver) = tokio::sync::oneshot::channel(); let challenge = HmacChallenge::new(&mut rand::rng(), 64); @@ -1232,7 +1722,11 @@ mod tests { #[tokio::test] async fn finalize_cancel() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let mut attempts = server.state.attempts.lock().unwrap(); let (sender, receiver) = tokio::sync::oneshot::channel(); let challenge = HmacChallenge::new(&mut rand::rng(), 64); @@ -1271,7 +1765,11 @@ mod tests { #[tokio::test] async fn finalize_cancel_at_intermediate() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let mut attempts = server.state.attempts.lock().unwrap(); let (sender, receiver) = tokio::sync::oneshot::channel(); let challenge = HmacChallenge::new(&mut rand::rng(), 64); @@ -1316,7 +1814,11 @@ mod tests { #[tokio::test] async fn finalize_unknown_session() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let response = server .get_router() @@ -1335,7 +1837,11 @@ mod tests { #[tokio::test] async fn finalize_cancel_unknown_session() { - let server = Server::new(ServerConfig { root_certificate: None }); + let server = Server::new(ServerConfig { + root_certificate: None, + advertised_endpoint: S2EndpointDescription::default(), + advertised_nodes: vec![], + }); let response = server .get_router() diff --git a/s2energy-connection/src/pairing/wire.rs b/s2energy-connection/src/pairing/wire.rs index 82ad4b2..18e3bf7 100644 --- a/s2energy-connection/src/pairing/wire.rs +++ b/s2energy-connection/src/pairing/wire.rs @@ -6,7 +6,10 @@ use subtle::ConstantTimeEq; use thiserror::Error; use tracing::info; -use crate::common::wire::{AccessToken, CommunicationProtocol, MessageVersion, S2EndpointDescription, S2NodeDescription}; +use crate::{ + S2NodeId, + common::wire::{AccessToken, CommunicationProtocol, MessageVersion, S2EndpointDescription, S2NodeDescription}, +}; #[derive(Error, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub(crate) enum PairingResponseErrorMessage { @@ -189,6 +192,25 @@ pub(crate) struct RequestConnectionDetailsRequest { pub server_hmac_challenge_response: HmacChallengeResponse, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PrePairingRequest { + pub client_s2_endpoint_description: S2EndpointDescription, + pub client_s2_node_description: S2NodeDescription, + // TODO: Update to Pairing S2 Node ID + #[serde(rename = "serverS2PairingNodeId")] + pub server_id: Option, +} + +#[derive(Serialize, Deserialize)] +pub(crate) struct CancelPrePairingRequest { + #[serde(rename = "clientS2NodeId")] + pub client_id: S2NodeId, + // TODO: Update to Pairing S2 Node ID + #[serde(rename = "serverS2PairingNodeId")] + pub server_id: Option, +} + /// Details the Connection client needs to set up an S2 session. #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")]