1mod 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#[derive(Clone, Default)]
18pub struct Ws {
19 origins: Vec<String>,
20 max_message_size: Option<usize>,
21}
22
23#[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 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}