Skip to main content

sova_ws/
lib.rs

1//! WebSocket plugin for Sova (HTTP upgrade + rooms hub).
2
3mod ext;
4mod hub;
5mod upgrade;
6
7pub use ext::WsRouteExt;
8pub use hub::{Hub, RoomHandle};
9pub use tokio_tungstenite::tungstenite::Message;
10pub use upgrade::{origin_allowed, upgrade_ws, WsSession};
11
12use std::sync::Arc;
13
14use sova_core::{App, Plugin};
15
16/// WebSocket plugin configuration.
17#[derive(Clone, Default)]
18pub struct Ws {
19    origins: Vec<String>,
20    max_message_size: Option<usize>,
21}
22
23/// Shared state installed by [`Ws`].
24#[derive(Clone)]
25pub struct WsShared {
26    pub hub: Hub,
27    pub config: Arc<WsConfig>,
28}
29
30#[derive(Clone)]
31pub struct WsConfig {
32    pub origins: Vec<String>,
33    pub max_message_size: Option<usize>,
34}
35
36impl Ws {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Allowed `Origin` values (CSWSH). Empty → allow all (dev).
42    pub fn origins(
43        mut self,
44        origins: impl IntoIterator<Item = impl Into<String>>,
45    ) -> Self {
46        self.origins = origins.into_iter().map(Into::into).collect();
47        self
48    }
49
50    pub fn max_message_size(mut self, n: usize) -> Self {
51        self.max_message_size = Some(n);
52        self
53    }
54}
55
56impl Plugin for Ws {
57    fn id(&self) -> &'static str {
58        "ws"
59    }
60
61    fn meta(&self) -> sova_core::PluginMeta {
62        sova_core::PluginMeta::new("WebSocket")
63            .description("WebSocket hub, origin allowlist, max message size")
64            .version(env!("CARGO_PKG_VERSION"))
65    }
66
67    fn install(self, app: &mut App) {
68        app.state(WsShared {
69            hub: Hub::new(),
70            config: Arc::new(WsConfig {
71                origins: self.origins,
72                max_message_size: self.max_message_size,
73            }),
74        });
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use http::HeaderMap;
82
83    #[test]
84    fn origin_empty_allowlist_allows_all() {
85        let mut headers = HeaderMap::new();
86        headers.insert("origin", "https://evil.test".parse().unwrap());
87        assert!(origin_allowed(&headers, &[]));
88    }
89
90    #[test]
91    fn origin_rejects_unknown() {
92        let mut headers = HeaderMap::new();
93        headers.insert("origin", "https://evil.test".parse().unwrap());
94        assert!(!origin_allowed(
95            &headers,
96            &["https://good.test".to_string()]
97        ));
98    }
99
100    #[test]
101    fn origin_accepts_match() {
102        let mut headers = HeaderMap::new();
103        headers.insert("origin", "https://good.test".parse().unwrap());
104        assert!(origin_allowed(
105            &headers,
106            &["https://good.test".to_string()]
107        ));
108    }
109}