Skip to main content

arora_websocket/
server.rs

1//! WebSocket server implementation.
2//!
3//! Provides a ready-to-use WebSocket server that handles the arora protocol.
4//! Each server supports at most one active client at a time.
5
6use std::collections::HashMap;
7use std::net::SocketAddr;
8use std::sync::Arc;
9
10use futures_util::{SinkExt, StreamExt};
11use log::{debug, error, info, warn};
12use tokio::net::{TcpListener, TcpStream};
13use tokio::sync::{broadcast, RwLock};
14use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
15use tokio_tungstenite::tungstenite::protocol::CloseFrame;
16use tokio_tungstenite::tungstenite::Message;
17use tokio_util::sync::CancellationToken;
18
19use crate::handlers::{GetSlotValuesHandler, OnClientConnectedHandler};
20use arora_types::value::Value;
21
22use crate::messages::{Incoming, Outgoing};
23use crate::registry::Registry;
24
25/// Callback for handling SetSlotValues messages.
26///
27/// Called when a valid SetSlotValues message is received.
28/// Return `Ok(())` to acknowledge success, or `Err(message)` to reject.
29pub type SetSlotValuesHandler =
30    Arc<dyn Fn(HashMap<String, Value>) -> Result<(), String> + Send + Sync>;
31
32/// Configuration for the WebSocket server.
33#[derive(Clone)]
34pub struct ServerConfig {
35    /// Port to listen on.
36    pub port: u16,
37    /// Address to bind to. Defaults to loopback: the protocol is
38    /// unauthenticated, so binding all interfaces is an explicit opt-in via
39    /// [`ServerConfig::bind_address`].
40    pub bind_address: String,
41    /// Whether to validate update paths against registered input slots.
42    pub validate_paths: bool,
43    /// Whether to serve the built-in control panel on plain HTTP requests.
44    pub serve_control_panel: bool,
45}
46
47impl Default for ServerConfig {
48    fn default() -> Self {
49        Self {
50            port: 9000,
51            bind_address: "127.0.0.1".to_string(),
52            validate_paths: true,
53            serve_control_panel: false,
54        }
55    }
56}
57
58impl ServerConfig {
59    /// Create a new config with the specified port.
60    pub fn with_port(port: u16) -> Self {
61        Self {
62            port,
63            ..Default::default()
64        }
65    }
66
67    /// Set the bind address.
68    pub fn bind_address(mut self, addr: impl Into<String>) -> Self {
69        self.bind_address = addr.into();
70        self
71    }
72
73    /// Set whether to validate update paths.
74    pub fn validate_paths(mut self, validate: bool) -> Self {
75        self.validate_paths = validate;
76        self
77    }
78
79    /// Enable or disable the built-in control panel served on plain HTTP requests.
80    pub fn serve_control_panel(mut self, enable: bool) -> Self {
81        self.serve_control_panel = enable;
82        self
83    }
84}
85
86/// WebSocket server for the arora protocol.
87///
88/// Handles connections, parses messages, and dispatches to registered handlers.
89/// Supports at most one active client at a time -- when a new client connects,
90/// the previous one is disconnected.
91pub struct AroraWSServer {
92    config: ServerConfig,
93    registry: Arc<Registry>,
94    set_slot_values_handler: RwLock<Option<SetSlotValuesHandler>>,
95    get_slot_values_handler: RwLock<Option<GetSlotValuesHandler>>,
96    on_client_connected_handler: RwLock<Option<OnClientConnectedHandler>>,
97    /// Cancel token for the single active client. When cancelled, the client is disconnected.
98    active_client: Arc<RwLock<Option<CancellationToken>>>,
99    is_running: RwLock<bool>,
100    /// Server-initiated pushes (Bridge::send_data) reach the active client here.
101    outbound_tx: broadcast::Sender<Outgoing>,
102}
103
104impl AroraWSServer {
105    /// Create a new server with the given configuration.
106    pub fn new(config: ServerConfig) -> Self {
107        Self {
108            config,
109            registry: Arc::new(Registry::new()),
110            set_slot_values_handler: RwLock::new(None),
111            get_slot_values_handler: RwLock::new(None),
112            on_client_connected_handler: RwLock::new(None),
113            active_client: Arc::new(RwLock::new(None)),
114            is_running: RwLock::new(false),
115            outbound_tx: broadcast::channel(256).0,
116        }
117    }
118
119    /// Create a new server with default configuration.
120    pub fn with_port(port: u16) -> Self {
121        Self::new(ServerConfig::with_port(port))
122    }
123
124    /// Push a server-initiated message to the connected client(s).
125    pub fn push(&self, msg: Outgoing) {
126        let _ = self.outbound_tx.send(msg);
127    }
128
129    /// Subscribe to the outbound push channel.
130    pub fn subscribe(&self) -> broadcast::Receiver<Outgoing> {
131        self.outbound_tx.subscribe()
132    }
133
134    /// Get a reference to the registry.
135    pub fn registry(&self) -> &Arc<Registry> {
136        &self.registry
137    }
138
139    /// Set the update handler callback.
140    /// This is called whenever a valid update message is received.
141    pub async fn set_set_slot_values_handler<F>(&self, handler: F)
142    where
143        F: Fn(HashMap<String, Value>) -> Result<(), String> + Send + Sync + 'static,
144    {
145        *self.set_slot_values_handler.write().await = Some(Arc::new(handler));
146    }
147
148    /// Set the get slot values handler callback.
149    /// This is called whenever a valid get slot values message is received.
150    pub async fn set_get_slot_values_handler(&self, handler: GetSlotValuesHandler) {
151        *self.get_slot_values_handler.write().await = Some(handler);
152    }
153
154    /// Set the handler called when a new client connects.
155    pub async fn set_on_client_connected_handler(&self, handler: OnClientConnectedHandler) {
156        *self.on_client_connected_handler.write().await = Some(handler);
157    }
158
159    /// Disconnect the current active client (if any).
160    pub async fn disconnect_client(&self) {
161        let mut guard = self.active_client.write().await;
162        if let Some(token) = guard.take() {
163            token.cancel();
164            info!(
165                "Disconnected active client on ws://{}:{}",
166                self.config.bind_address, self.config.port
167            );
168        }
169    }
170
171    /// Check if the server is running.
172    pub async fn is_running(&self) -> bool {
173        *self.is_running.read().await
174    }
175
176    /// Get the configured port.
177    pub fn port(&self) -> u16 {
178        self.config.port
179    }
180
181    /// Run the server until the cancellation token is triggered.
182    pub async fn run(&self, cancel_token: CancellationToken) -> Result<(), String> {
183        let addr = format!("{}:{}", self.config.bind_address, self.config.port);
184        let listener = TcpListener::bind(&addr)
185            .await
186            .map_err(|e| format!("Failed to bind to {}: {}", addr, e))?;
187
188        info!("Arora WebSocket server listening on ws://{}", addr);
189        if self.config.serve_control_panel {
190            info!("Control panel available at http://{}", addr);
191        }
192        *self.is_running.write().await = true;
193
194        let serve_control_panel = self.config.serve_control_panel;
195        let validate_paths = self.config.validate_paths;
196        let conn_id = self.connection_id();
197        let bind_addr = self.config.bind_address.clone();
198        let port = self.config.port;
199
200        // Snapshot handlers once -- they are set during setup_all() and never change.
201        let set_handler = self.set_slot_values_handler.read().await.clone();
202        let get_handler = self.get_slot_values_handler.read().await.clone();
203        let on_connected = self.on_client_connected_handler.read().await.clone();
204
205        loop {
206            tokio::select! {
207                result = listener.accept() => {
208                    match result {
209                        Ok((stream, peer_addr)) => {
210                            // Spawn a task per connection so the accept loop never blocks.
211                            // (peek can block if a client connects without sending data.)
212                            let active_client = self.active_client.clone();
213                            let registry = self.registry.clone();
214                            let set_handler = set_handler.clone();
215                            let get_handler = get_handler.clone();
216                            let on_connected = on_connected.clone();
217                            let conn_id = conn_id.clone();
218                            let bind_addr = bind_addr.clone();
219                            let parent_token = cancel_token.clone();
220                            let outbound_tx = self.outbound_tx.clone();
221
222                            tokio::spawn(async move {
223                                // Peek with timeout to classify the connection
224                                let is_ws_upgrade = {
225                                    let mut peek_buf = [0u8; 4096];
226                                    match tokio::time::timeout(
227                                        std::time::Duration::from_secs(5),
228                                        stream.peek(&mut peek_buf),
229                                    ).await {
230                                        Ok(Ok(n)) => {
231                                            let req = String::from_utf8_lossy(&peek_buf[..n]);
232                                            let lower = req.to_ascii_lowercase();
233                                            lower.contains("upgrade") && lower.contains("websocket")
234                                        }
235                                        Ok(Err(e)) => {
236                                            error!("Failed to peek connection from {}: {}", peer_addr, e);
237                                            return;
238                                        }
239                                        Err(_) => {
240                                            debug!("Connection from {} sent no data within timeout", peer_addr);
241                                            return;
242                                        }
243                                    }
244                                };
245
246                                if !is_ws_upgrade {
247                                    if serve_control_panel {
248                                        serve_control_panel_http(stream).await;
249                                    }
250                                    return;
251                                }
252
253                                // WebSocket: enforce exclusive client policy
254                                let client_token = parent_token.child_token();
255                                {
256                                    let mut guard = active_client.write().await;
257                                    if let Some(old) = guard.take() {
258                                        old.cancel();
259                                        info!("Disconnected active client on ws://{}:{}", bind_addr, port);
260                                    }
261                                    *guard = Some(client_token.clone());
262                                }
263
264                                // Notify the on_client_connected handler
265                                if let Some(ref handler) = on_connected {
266                                    handler(conn_id);
267                                }
268
269                                handle_connection(
270                                    stream, peer_addr, registry,
271                                    set_handler, get_handler,
272                                    validate_paths, client_token, active_client,
273                                    outbound_tx,
274                                ).await;
275                            });
276                        }
277                        Err(e) => {
278                            error!("Failed to accept connection: {}", e);
279                        }
280                    }
281                }
282                _ = cancel_token.cancelled() => {
283                    info!("Arora WebSocket server shutting down");
284                    // Disconnect the active client on shutdown
285                    self.disconnect_client().await;
286                    break;
287                }
288            }
289        }
290
291        *self.is_running.write().await = false;
292        Ok(())
293    }
294
295    /// Get the connection identifier.
296    pub fn connection_id(&self) -> String {
297        format!("ws://127.0.0.1:{}", self.config.port)
298    }
299}
300
301/// Handle a single WebSocket connection.
302#[allow(clippy::too_many_arguments)]
303async fn handle_connection(
304    stream: TcpStream,
305    addr: SocketAddr,
306    registry: Arc<Registry>,
307    set_slot_values_handler: Option<SetSlotValuesHandler>,
308    get_slot_values_handler: Option<GetSlotValuesHandler>,
309    validate_paths: bool,
310    client_token: CancellationToken,
311    active_client: Arc<RwLock<Option<CancellationToken>>>,
312    outbound_tx: broadcast::Sender<Outgoing>,
313) {
314    info!("New WebSocket connection from: {}", addr);
315
316    let ws_config = tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
317        // The largest legitimate message is a few KiB; cap far below the
318        // 64 MiB tungstenite default so a client cannot force huge allocations.
319        max_message_size: Some(1 << 20),
320        max_frame_size: Some(256 << 10),
321        ..Default::default()
322    };
323    let ws_stream = match tokio_tungstenite::accept_async_with_config(stream, Some(ws_config)).await
324    {
325        Ok(ws) => ws,
326        Err(e) => {
327            error!("Error during WebSocket handshake: {}", e);
328            return;
329        }
330    };
331
332    let (mut write, mut read) = ws_stream.split();
333    let mut outbound_rx = outbound_tx.subscribe();
334
335    loop {
336        tokio::select! {
337            msg = read.next() => {
338                match msg {
339                    Some(Ok(Message::Text(text))) => {
340                        debug!("Received message: {}", text);
341
342                        let response = match serde_json::from_str::<Incoming>(&text) {
343                            Ok(incoming) => {
344                                process_message(incoming, &registry, &set_slot_values_handler, &get_slot_values_handler, validate_paths).await
345                            }
346                            Err(e) => {
347                                warn!("Failed to parse message: {}", e);
348                                Outgoing::Error {
349                                    request_id: None,
350                                    message: format!("Invalid message format: {}", e),
351                                }
352                            }
353                        };
354
355                        let response_text = match serde_json::to_string(&response) {
356                            Ok(text) => text,
357                            Err(e) => {
358                                error!("Failed to serialize response: {}", e);
359                                break;
360                            }
361                        };
362                        if let Err(e) = write.send(Message::Text(response_text)).await {
363                            error!("Failed to send response: {}", e);
364                            break;
365                        }
366                    }
367                    Some(Ok(Message::Close(_))) => {
368                        info!("Client {} disconnected", addr);
369                        break;
370                    }
371                    Some(Ok(Message::Ping(data))) => {
372                        if let Err(e) = write.send(Message::Pong(data)).await {
373                            error!("Failed to send pong: {}", e);
374                            break;
375                        }
376                    }
377                    Some(Ok(_)) => {
378                        // Ignore other message types
379                    }
380                    Some(Err(e)) => {
381                        error!("Error reading message: {}", e);
382                        break;
383                    }
384                    None => {
385                        // Stream ended
386                        break;
387                    }
388                }
389            }
390            pushed = outbound_rx.recv() => {
391                match pushed {
392                    Ok(msg) => {
393                        let text = match serde_json::to_string(&msg) {
394                            Ok(text) => text,
395                            Err(e) => {
396                                error!("Failed to serialize push message: {}", e);
397                                continue;
398                            }
399                        };
400                        if let Err(e) = write.send(Message::Text(text)).await {
401                            error!("Failed to push message: {}", e);
402                            break;
403                        }
404                    }
405                    Err(broadcast::error::RecvError::Closed) => break,
406                    Err(broadcast::error::RecvError::Lagged(_)) => {}
407                }
408            }
409            _ = client_token.cancelled() => {
410                info!("Client {} disconnected by server (exclusive client policy)", addr);
411                // Send a close frame to the client
412                let close_frame = CloseFrame {
413                    code: CloseCode::Normal,
414                    reason: "Another client connected".into(),
415                };
416                let _ = write.send(Message::Close(Some(close_frame))).await;
417                break;
418            }
419        }
420    }
421
422    // Clear the active client only if this was a natural disconnect.
423    // If our token was cancelled, we were replaced by a new client -- don't touch active_client.
424    if !client_token.is_cancelled() {
425        let mut guard = active_client.write().await;
426        *guard = None;
427    }
428
429    info!("Connection closed for: {}", addr);
430}
431
432/// Serve the built-in control panel HTML over a plain HTTP response.
433async fn serve_control_panel_http(mut stream: TcpStream) {
434    use tokio::io::{AsyncReadExt, AsyncWriteExt};
435
436    const HTML: &str = include_str!("control_panel.html");
437
438    // Read and consume the HTTP request from the buffer
439    let mut buf = vec![0u8; 4096];
440    let _ = stream.read(&mut buf).await;
441
442    let body = HTML.as_bytes();
443    let header = format!(
444        "HTTP/1.1 200 OK\r\n\
445         Content-Type: text/html; charset=utf-8\r\n\
446         Content-Length: {}\r\n\
447         Connection: close\r\n\
448         \r\n",
449        body.len()
450    );
451
452    let _ = stream.write_all(header.as_bytes()).await;
453    let _ = stream.write_all(body).await;
454}
455
456/// Process an incoming message and return the response.
457///
458/// This function is public so it can be reused by other connection types
459/// (e.g., WebAppServer) that share the same arora protocol.
460pub async fn process_message(
461    incoming: Incoming,
462    registry: &Registry,
463    set_slot_values_handler: &Option<SetSlotValuesHandler>,
464    get_slot_values_handler: &Option<GetSlotValuesHandler>,
465    validate_paths: bool,
466) -> Outgoing {
467    match incoming {
468        Incoming::SetSlotValues { values } => {
469            // Validate paths if enabled
470            if validate_paths {
471                let input_paths = registry.get_input_paths().await;
472                let invalid_paths: Vec<&str> = values
473                    .keys()
474                    .filter(|path| !input_paths.iter().any(|p| p == *path))
475                    .map(|s| s.as_str())
476                    .collect();
477
478                if !invalid_paths.is_empty() {
479                    warn!("Invalid paths in SetSlotValues: {:?}", invalid_paths);
480                    return Outgoing::SetSlotValuesResp {
481                        success: false,
482                        message: Some(format!(
483                            "Unknown input path(s): {}",
484                            invalid_paths.join(", ")
485                        )),
486                    };
487                }
488            }
489
490            // Call SetSlotValues handler if registered
491            if let Some(handler) = set_slot_values_handler {
492                match handler(values) {
493                    Ok(()) => {
494                        debug!("SetSlotValues handled successfully");
495                        Outgoing::SetSlotValuesResp {
496                            success: true,
497                            message: None,
498                        }
499                    }
500                    Err(e) => {
501                        error!("SetSlotValues handler error: {}", e);
502                        Outgoing::SetSlotValuesResp {
503                            success: false,
504                            message: Some(e),
505                        }
506                    }
507                }
508            } else {
509                // No handler registered, just acknowledge
510                debug!("No SetSlotValues handler registered, acknowledging");
511                Outgoing::SetSlotValuesResp {
512                    success: true,
513                    message: None,
514                }
515            }
516        }
517
518        Incoming::GetSlotValues { slots } => {
519            // Call GetSlotValues handler if registered
520            if let Some(handler) = get_slot_values_handler {
521                let values = handler(slots).await;
522                Outgoing::GetSlotValuesResp { values }
523            } else {
524                // No handler registered, return empty values
525                debug!("No GetSlotValues handler registered, returning empty values");
526                Outgoing::GetSlotValuesResp {
527                    values: HashMap::new(),
528                }
529            }
530        }
531
532        Incoming::ListSlots { path } => {
533            let slots = registry.get_slots_filtered(path.as_deref()).await;
534            Outgoing::ListSlotsResp { slots }
535        }
536
537        Incoming::ListMethods { path } => {
538            let methods = registry.get_methods_filtered(path.as_deref()).await;
539            Outgoing::ListMethodsResp { methods }
540        }
541
542        Incoming::Invoke {
543            method,
544            args,
545            request_id,
546        } => {
547            let result = registry.invoke_method(&method, args).await;
548            Outgoing::InvokeResp {
549                success: result.success,
550                request_id,
551                value: result.value,
552                message: result.message,
553            }
554        }
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561
562    #[test]
563    fn test_server_config_default() {
564        let config = ServerConfig::default();
565        assert_eq!(config.port, 9000);
566        assert_eq!(config.bind_address, "127.0.0.1");
567        assert!(config.validate_paths);
568        assert!(!config.serve_control_panel);
569    }
570
571    #[test]
572    fn test_server_config_builder() {
573        let config = ServerConfig::with_port(8080)
574            .bind_address("127.0.0.1")
575            .validate_paths(false)
576            .serve_control_panel(true);
577
578        assert_eq!(config.port, 8080);
579        assert_eq!(config.bind_address, "127.0.0.1");
580        assert!(!config.validate_paths);
581        assert!(config.serve_control_panel);
582    }
583}