1use 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#[derive(Clone)]
27pub struct ServerConfig {
28 pub port: u16,
30 pub bind_address: String,
34 pub validate_paths: bool,
36 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 pub fn with_port(port: u16) -> Self {
54 Self {
55 port,
56 ..Default::default()
57 }
58 }
59
60 pub fn bind_address(mut self, addr: impl Into<String>) -> Self {
62 self.bind_address = addr.into();
63 self
64 }
65
66 pub fn validate_paths(mut self, validate: bool) -> Self {
68 self.validate_paths = validate;
69 self
70 }
71
72 pub fn serve_control_panel(mut self, enable: bool) -> Self {
74 self.serve_control_panel = enable;
75 self
76 }
77}
78
79pub 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 active_client: Arc<RwLock<Option<CancellationToken>>>,
92 is_running: RwLock<bool>,
93 outbound_tx: broadcast::Sender<Outgoing>,
95}
96
97impl AroraWSServer {
98 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 pub fn with_port(port: u16) -> Self {
114 Self::new(ServerConfig::with_port(port))
115 }
116
117 pub fn push(&self, msg: Outgoing) {
119 let _ = self.outbound_tx.send(msg);
120 }
121
122 pub fn subscribe(&self) -> broadcast::Receiver<Outgoing> {
124 self.outbound_tx.subscribe()
125 }
126
127 pub fn registry(&self) -> &Arc<Registry> {
129 &self.registry
130 }
131
132 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 pub async fn set_read_values_handler(&self, handler: ReadValuesHandler) {
144 *self.read_values_handler.write().await = Some(handler);
145 }
146
147 pub async fn set_on_client_connected_handler(&self, handler: OnClientConnectedHandler) {
149 *self.on_client_connected_handler.write().await = Some(handler);
150 }
151
152 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 pub async fn is_running(&self) -> bool {
166 *self.is_running.read().await
167 }
168
169 pub fn port(&self) -> u16 {
171 self.config.port
172 }
173
174 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 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 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 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 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 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 self.disconnect_client().await;
279 break;
280 }
281 }
282 }
283
284 *self.is_running.write().await = false;
285 Ok(())
286 }
287
288 pub fn connection_id(&self) -> String {
290 format!("ws://127.0.0.1:{}", self.config.port)
291 }
292}
293
294#[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 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, ®istry, &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 }
373 Some(Err(e)) => {
374 error!("Error reading message: {}", e);
375 break;
376 }
377 None => {
378 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 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 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
425async 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 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
449pub 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 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 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 debug!("No WriteValues handler registered, acknowledging");
504 Outgoing::WriteValuesResp {
505 success: true,
506 message: None,
507 }
508 }
509 }
510
511 Incoming::ReadValues { keys } => {
512 if let Some(handler) = read_values_handler {
514 let values = handler(keys).await;
515 Outgoing::ReadValuesResp { values }
516 } else {
517 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}