Skip to main content

arora_websocket/
server.rs

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