tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Sutichaykuna

⎇ a97f6b98f39e1ab9d4982ce0fda907a19c1ffa83 crates/core/brahman-handshake/src/client.rs Rust · 312 siq'ikuna · 11.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(brahman-handshake): ListMatches endpoint + timeline en broker-explorer a97f6b98f · Sergio ·

1//! Cliente de handshake. Conecta a un Unix socket y mantiene la sesión.
2
3use std::collections::VecDeque;
4use std::path::Path;
5use std::time::Duration;
6
7use brahman_card::{Card, WitInterface, CARD_SCHEMA_VERSION};
8use brahman_net::Keypair;
9use thiserror::Error;
10use tokio::io::{AsyncRead, AsyncWrite};
11use tokio::net::UnixStream;
12
13use crate::codec::{read_frame, write_frame};
14use crate::identity::SessionCert;
15use crate::messages::{Farewell, Frame, HandshakeError, Hello, HelloAck, MatchEvent, Ping, SessionId};
16use crate::signature::{sign_hello, SignatureError};
17
18/// Errores del cliente.
19#[derive(Debug, Error)]
20pub enum ClientError {
21    #[error("E/S: {0}")]
22    Io(#[from] std::io::Error),
23
24    /// El servidor respondió con un error explícito.
25    #[error("servidor: {0}")]
26    Server(#[source] HandshakeError),
27
28    /// El servidor envió un frame que no esperábamos en este punto del protocolo.
29    #[error("frame inesperado: {got}")]
30    UnexpectedFrame { got: &'static str },
31
32    /// La Card que el cliente intentó enviar no pasa su propia validación.
33    #[error("card inválida pre-envío: {0}")]
34    InvalidCard(String),
35
36    /// Firma del Hello falló al construirse (rara — sólo puede pasar
37    /// si la keypair pasada está en un estado inválido).
38    #[error("firma del Hello falló: {0}")]
39    Signature(#[from] SignatureError),
40}
41
42/// Cliente conectado y autenticado. Tras `connect` ya completó el handshake
43/// y tiene su `SessionId`. Los `MatchEvent` recibidos durante operaciones
44/// request/response se buferean en `pending_events` y se obtienen vía
45/// [`Client::take_event`] o [`Client::await_event`].
46///
47/// Genérico sobre el transport (`AsyncRead + AsyncWrite + Unpin + Send`):
48/// funciona indistintamente sobre `UnixStream` (path local) o sobre un
49/// stream libp2p wrapped con `tokio_util::compat` (path remoto, vía
50/// `brahman_handshake::network`).
51#[derive(Debug)]
52pub struct Client<S = UnixStream> {
53    stream: S,
54    session: SessionId,
55    server_info: HelloAck,
56    pending_events: VecDeque<MatchEvent>,
57}
58
59impl Client<UnixStream> {
60    /// Conecta como módulo agnóstico (sin WIT) sobre Unix socket.
61    /// Equivalente a `connect_with(path, card, None)`.
62    pub async fn connect(path: impl AsRef<Path>, card: Card) -> Result<Self, ClientError> {
63        Self::connect_with(path, card, None).await
64    }
65
66    /// Conecta al socket Unix enviando Hello con la Card dada y
67    /// opcionalmente una `WitInterface` ya extraída. Si `wit` es `Some`,
68    /// el server registra el módulo como "consciente".
69    pub async fn connect_with(
70        path: impl AsRef<Path>,
71        card: Card,
72        wit: Option<WitInterface>,
73    ) -> Result<Self, ClientError> {
74        let stream = UnixStream::connect(path).await?;
75        Self::connect_with_stream(stream, card, wit).await
76    }
77}
78
79impl<S> Client<S>
80where
81    S: AsyncRead + AsyncWrite + Unpin + Send,
82{
83    /// Constructor genérico sobre un stream ya abierto, **sin firma**.
84    /// Apto para path Unix (donde SO_PEERCRED del kernel ya autentica)
85    /// o tests in-memory. Para libp2p remoto usá
86    /// [`connect_with_stream_signed`](Self::connect_with_stream_signed) —
87    /// el server libp2p rechaza Hello sin firma.
88    pub async fn connect_with_stream(
89        stream: S,
90        card: Card,
91        wit: Option<WitInterface>,
92    ) -> Result<Self, ClientError> {
93        Self::connect_inner(stream, card, wit, None, None).await
94    }
95
96    /// Igual que `connect_with_stream` pero firma el Hello con
97    /// `keypair`. Usar para conexiones libp2p donde el server exige
98    /// firma. La public key derivada de `keypair` debe coincidir con
99    /// el `peer_id` libp2p autenticado por Noise — típicamente la
100    /// keypair pasada a [`brahman_net::BrahmanNet::with_keypair`].
101    pub async fn connect_with_stream_signed(
102        stream: S,
103        card: Card,
104        wit: Option<WitInterface>,
105        keypair: &Keypair,
106    ) -> Result<Self, ClientError> {
107        Self::connect_inner(stream, card, wit, Some(keypair), None).await
108    }
109
110    /// Igual que `connect_with_stream_signed` pero además adjunta un
111    /// `SessionCert` que vincula la session keypair a una identity
112    /// master estable. El server, al recibir el cert, evalúa la
113    /// política de admisión contra el `master_peer_id` (no contra
114    /// el session peer_id) — permitiendo rotar la session sin perder
115    /// la identidad reconocida en allowlists remotas.
116    pub async fn connect_with_stream_signed_with_cert(
117        stream: S,
118        card: Card,
119        wit: Option<WitInterface>,
120        session_keypair: &Keypair,
121        identity_cert: SessionCert,
122    ) -> Result<Self, ClientError> {
123        Self::connect_inner(stream, card, wit, Some(session_keypair), Some(identity_cert)).await
124    }
125
126    async fn connect_inner(
127        mut stream: S,
128        card: Card,
129        wit: Option<WitInterface>,
130        keypair: Option<&Keypair>,
131        identity_cert: Option<SessionCert>,
132    ) -> Result<Self, ClientError> {
133        card.validate()
134            .map_err(|e| ClientError::InvalidCard(e.to_string()))?;
135
136        let wire_card = brahman_card::WireCard::from(card);
137        let signature = match keypair {
138            Some(kp) => Some(sign_hello(kp, &wire_card, &wit)?),
139            None => None,
140        };
141
142        let hello = Hello {
143            schema_version: CARD_SCHEMA_VERSION,
144            protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
145            card: wire_card,
146            wit,
147            signature,
148            identity_cert,
149        };
150        write_frame(&mut stream, &Frame::Hello(hello)).await?;
151
152        let frame = read_frame(&mut stream).await?;
153        let ack = match frame {
154            Frame::HelloAck(a) => a,
155            Frame::Error(e) => return Err(ClientError::Server(e)),
156            Frame::Hello(_) => return Err(ClientError::UnexpectedFrame { got: "Hello" }),
157            Frame::Ping(_) => return Err(ClientError::UnexpectedFrame { got: "Ping" }),
158            Frame::Pong(_) => return Err(ClientError::UnexpectedFrame { got: "Pong" }),
159            Frame::Farewell(_) => return Err(ClientError::UnexpectedFrame { got: "Farewell" }),
160            Frame::MatchEvent(_) => {
161                return Err(ClientError::UnexpectedFrame {
162                    got: "MatchEvent (pre-handshake)",
163                });
164            }
165            Frame::ListSessions(_) => {
166                return Err(ClientError::UnexpectedFrame {
167                    got: "ListSessions (pre-handshake)",
168                });
169            }
170            Frame::SessionList(_) => {
171                return Err(ClientError::UnexpectedFrame {
172                    got: "SessionList (pre-handshake)",
173                });
174            }
175            Frame::ListMatches(_) => {
176                return Err(ClientError::UnexpectedFrame {
177                    got: "ListMatches (pre-handshake)",
178                });
179            }
180            Frame::MatchList(_) => {
181                return Err(ClientError::UnexpectedFrame {
182                    got: "MatchList (pre-handshake)",
183                });
184            }
185        };
186        Ok(Self {
187            stream,
188            session: ack.session,
189            server_info: ack,
190            pending_events: VecDeque::new(),
191        })
192    }
193
194    /// `SessionId` asignado por el servidor.
195    pub fn session(&self) -> SessionId {
196        self.session
197    }
198
199    /// Información del servidor recibida en el handshake.
200    pub fn server_info(&self) -> &HelloAck {
201        &self.server_info
202    }
203
204    /// Envía un Ping y devuelve el timestamp del servidor. Los frames
205    /// `MatchEvent` que lleguen mezclados se buferean en `pending_events`.
206    pub async fn ping(&mut self) -> Result<u64, ClientError> {
207        write_frame(
208            &mut self.stream,
209            &Frame::Ping(Ping {
210                session: self.session,
211            }),
212        )
213        .await?;
214        loop {
215            match read_frame(&mut self.stream).await? {
216                Frame::Pong(p) => return Ok(p.timestamp_ms),
217                Frame::MatchEvent(ev) => self.pending_events.push_back(ev),
218                Frame::Error(e) => return Err(ClientError::Server(e)),
219                _ => return Err(ClientError::UnexpectedFrame { got: "non-pong" }),
220            }
221        }
222    }
223
224    /// Saca un evento pendiente del buffer, sin bloquear ni leer del wire.
225    pub fn take_event(&mut self) -> Option<MatchEvent> {
226        self.pending_events.pop_front()
227    }
228
229    /// Espera un `MatchEvent` con timeout. Drena primero el buffer; si
230    /// está vacío, lee del wire hasta el timeout. Otros frames recibidos
231    /// (Pong huérfano, Error) cortan la espera con error.
232    pub async fn await_event(
233        &mut self,
234        timeout: Duration,
235    ) -> Result<Option<MatchEvent>, ClientError> {
236        if let Some(ev) = self.pending_events.pop_front() {
237            return Ok(Some(ev));
238        }
239        match tokio::time::timeout(timeout, read_frame(&mut self.stream)).await {
240            Err(_) => Ok(None),
241            Ok(Err(e)) => Err(ClientError::Io(e)),
242            Ok(Ok(Frame::MatchEvent(ev))) => Ok(Some(ev)),
243            Ok(Ok(Frame::Error(e))) => Err(ClientError::Server(e)),
244            Ok(Ok(_)) => Err(ClientError::UnexpectedFrame {
245                got: "non-event en await_event",
246            }),
247        }
248    }
249
250    /// Pide al servidor el listado de sesiones activas. Pensado para
251    /// observadores (broker-explorer, CLIs de diagnóstico). Como
252    /// `ping`, los `MatchEvent` que lleguen intercalados se bufean
253    /// en `pending_events` y no rompen la respuesta.
254    pub async fn list_sessions(&mut self) -> Result<crate::messages::SessionList, ClientError> {
255        write_frame(
256            &mut self.stream,
257            &Frame::ListSessions(crate::messages::ListSessions {
258                session: self.session,
259            }),
260        )
261        .await?;
262        loop {
263            match read_frame(&mut self.stream).await? {
264                Frame::SessionList(list) => return Ok(list),
265                Frame::MatchEvent(ev) => self.pending_events.push_back(ev),
266                Frame::Error(e) => return Err(ClientError::Server(e)),
267                _ => {
268                    return Err(ClientError::UnexpectedFrame {
269                        got: "non-session-list",
270                    });
271                }
272            }
273        }
274    }
275
276    /// Pide al servidor el listado de matches actuales del broker
277    /// (consumer↔producer pares con tipo y estrategia). Mismo patrón
278    /// de drenado de `MatchEvent`s intermedios.
279    pub async fn list_matches(&mut self) -> Result<crate::messages::MatchList, ClientError> {
280        write_frame(
281            &mut self.stream,
282            &Frame::ListMatches(crate::messages::ListMatches {
283                session: self.session,
284            }),
285        )
286        .await?;
287        loop {
288            match read_frame(&mut self.stream).await? {
289                Frame::MatchList(list) => return Ok(list),
290                Frame::MatchEvent(ev) => self.pending_events.push_back(ev),
291                Frame::Error(e) => return Err(ClientError::Server(e)),
292                _ => {
293                    return Err(ClientError::UnexpectedFrame {
294                        got: "non-match-list",
295                    });
296                }
297            }
298        }
299    }
300
301    /// Cierre cooperativo. Consume el cliente.
302    pub async fn farewell(mut self) -> Result<(), ClientError> {
303        write_frame(
304            &mut self.stream,
305            &Frame::Farewell(Farewell {
306                session: self.session,
307            }),
308        )
309        .await?;
310        Ok(())
311    }
312}