logicaffeine_cli/browser/
server.rs1use std::collections::{BTreeMap, HashMap};
14use std::io::{BufRead, BufReader, Read, Write};
15use std::net::{Shutdown, TcpListener, TcpStream};
16use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
17use std::sync::{Arc, Mutex};
18use std::time::Duration;
19
20use base64::Engine as _;
21use logicaffeine_system::relay_proto::RelayFrame;
22
23#[derive(Clone, Default)]
26pub struct RelayHub {
27 state: Arc<Mutex<HubState>>,
28}
29
30impl std::fmt::Debug for RelayHub {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 let st = self.state.lock().unwrap();
33 f.debug_struct("RelayHub")
34 .field("connections", &st.conns.len())
35 .field("topics", &st.subs.keys().collect::<Vec<_>>())
36 .finish()
37 }
38}
39
40#[derive(Default)]
41struct HubState {
42 conns: HashMap<String, Arc<Mutex<TcpStream>>>,
44 subs: BTreeMap<String, Vec<String>>,
45 counts: HashMap<String, (u64, u64)>,
47 received: Vec<RelayFrame>,
49}
50
51impl RelayHub {
52 pub fn deliver(&self, topic: &str, data: &[u8]) {
55 let mut st = self.state.lock().unwrap();
56 let to: Vec<String> = st.subs.get(topic).cloned().unwrap_or_default();
57 let frame = RelayFrame::Event {
58 topic: topic.to_string(),
59 data: data.to_vec(),
60 };
61 for conn in &to {
62 st.write(conn, &frame);
63 }
64 }
65
66 pub fn subscribers(&self, topic: &str) -> usize {
68 self.state
69 .lock()
70 .unwrap()
71 .subs
72 .get(topic)
73 .map_or(0, Vec::len)
74 }
75
76 pub fn received(&self) -> Vec<RelayFrame> {
78 self.state.lock().unwrap().received.clone()
79 }
80
81 pub(crate) fn counts(&self, token: &str) -> (u64, u64) {
83 self.state
84 .lock()
85 .unwrap()
86 .counts
87 .get(token)
88 .copied()
89 .unwrap_or((0, 0))
90 }
91
92 fn join(&self, token: &str, writer: TcpStream) {
93 self.state
94 .lock()
95 .unwrap()
96 .conns
97 .insert(token.to_string(), Arc::new(Mutex::new(writer)));
98 }
99
100 fn receive(&self, conn: &str, message: &[u8]) {
103 let mut st = self.state.lock().unwrap();
104 st.counts.entry(conn.to_string()).or_default().0 += 1;
105 let frame = RelayFrame::from_bytes(message);
106 st.received.extend(frame.clone());
107 match frame {
108 Some(RelayFrame::Subscribe { topic }) => {
109 let subs = st.subs.entry(topic.clone()).or_default();
110 if !subs.iter().any(|c| c == conn) {
111 subs.push(conn.to_string());
112 }
113 st.write(conn, &RelayFrame::SubAck { topic });
114 }
115 Some(RelayFrame::Unsubscribe { topic }) => {
116 if let Some(subs) = st.subs.get_mut(&topic) {
117 subs.retain(|c| c != conn);
118 }
119 }
120 Some(RelayFrame::Publish { topic, data }) => {
121 let to: Vec<String> = st
122 .subs
123 .get(&topic)
124 .into_iter()
125 .flatten()
126 .filter(|c| *c != conn)
127 .cloned()
128 .collect();
129 let event = RelayFrame::Event { topic, data };
130 for c in &to {
131 st.write(c, &event);
132 }
133 }
134 Some(RelayFrame::SubAck { .. } | RelayFrame::Event { .. }) | None => {}
135 }
136 }
137
138 fn leave(&self, conn: &str) {
139 let mut st = self.state.lock().unwrap();
140 st.conns.remove(conn);
141 for subs in st.subs.values_mut() {
142 subs.retain(|c| c != conn);
143 }
144 }
145}
146
147impl HubState {
148 fn write(&mut self, conn: &str, frame: &RelayFrame) {
150 let Some(writer) = self.conns.get(conn).cloned() else {
151 return;
152 };
153 let payload = frame.to_bytes();
154 let mut out = vec![0x82];
155 match payload.len() {
156 n if n < 126 => out.push(n as u8),
157 n if n <= 0xFFFF => {
158 out.push(126);
159 out.extend_from_slice(&(n as u16).to_be_bytes());
160 }
161 n => {
162 out.push(127);
163 out.extend_from_slice(&(n as u64).to_be_bytes());
164 }
165 }
166 out.extend_from_slice(&payload);
167 if writer.lock().unwrap().write_all(&out).is_ok() {
168 self.counts.entry(conn.to_string()).or_default().1 += 1;
169 }
170 }
171}
172
173pub(crate) struct StaticServer {
174 origin: String,
175 stop: Arc<AtomicBool>,
176 port: u16,
177 hub: RelayHub,
178}
179
180impl StaticServer {
181 pub(crate) fn start(
184 files: BTreeMap<String, (String, Vec<u8>)>,
185 hub: RelayHub,
186 ) -> std::io::Result<StaticServer> {
187 let listener = TcpListener::bind("127.0.0.1:0")?;
188 let port = listener.local_addr()?.port();
189 let stop = Arc::new(AtomicBool::new(false));
190 let files = Arc::new(files);
191 let halt = Arc::clone(&stop);
192 let relay = hub.clone();
193 std::thread::Builder::new()
194 .name("page-server".into())
195 .spawn(move || {
196 for conn in listener.incoming() {
197 if halt.load(Ordering::SeqCst) {
198 break;
199 }
200 let Ok(conn) = conn else { continue };
201 let files = Arc::clone(&files);
202 let relay = relay.clone();
203 let _ = std::thread::Builder::new()
204 .name("page-server-conn".into())
205 .spawn(move || respond(conn, &files, &relay));
206 }
207 })?;
208 Ok(StaticServer {
209 origin: format!("http://127.0.0.1:{port}"),
210 stop,
211 port,
212 hub,
213 })
214 }
215
216 pub(crate) fn origin(&self) -> &str {
218 &self.origin
219 }
220
221 pub(crate) fn relay(&self) -> &RelayHub {
223 &self.hub
224 }
225}
226
227impl Drop for StaticServer {
228 fn drop(&mut self) {
229 self.stop.store(true, Ordering::SeqCst);
230 let _ = TcpStream::connect(("127.0.0.1", self.port));
232 }
233}
234
235fn respond(conn: TcpStream, files: &BTreeMap<String, (String, Vec<u8>)>, hub: &RelayHub) {
236 let _ = conn.set_read_timeout(Some(Duration::from_secs(10)));
237 let mut reader = BufReader::new(&conn);
238 let mut request_line = String::new();
239 if reader.read_line(&mut request_line).is_err() {
240 return;
241 }
242 let mut websocket_key = None;
243 loop {
244 let mut header = String::new();
245 match reader.read_line(&mut header) {
246 Ok(0) => break,
247 Ok(_) if header == "\r\n" || header == "\n" => break,
248 Ok(_) => {
249 if let Some((name, value)) = header.split_once(':') {
250 if name.trim().eq_ignore_ascii_case("sec-websocket-key") {
251 websocket_key = Some(value.trim().to_string());
252 }
253 }
254 }
255 Err(_) => return,
256 }
257 }
258 let mut parts = request_line.split_whitespace();
259 let method = parts.next().unwrap_or("");
260 let target = parts.next().unwrap_or("/");
261 let path = target.split(['?', '#']).next().unwrap_or("/");
262 if path == "/_relay" {
263 if let Some(key) = websocket_key {
264 drop(reader);
265 let token = target
266 .split_once('?')
267 .and_then(|(_, q)| q.split('&').find_map(|kv| kv.strip_prefix("harness=")))
268 .map(str::to_string)
269 .unwrap_or_else(|| format!("unnamed-{}", UNNAMED.fetch_add(1, Ordering::SeqCst)));
270 relay(conn, &key, &token, hub);
271 }
272 return;
273 }
274 let found = if method == "GET" || method == "HEAD" {
275 files.get(path)
276 } else {
277 None
278 };
279 let mut out = &conn;
280 let head = match found {
281 Some((content_type, bytes)) => format!(
282 "HTTP/1.1 200 OK\r\ncontent-type: {content_type}\r\ncontent-length: {}\r\ncache-control: no-store\r\nconnection: close\r\n\r\n",
283 bytes.len()
284 ),
285 None => "HTTP/1.1 404 Not Found\r\ncontent-type: text/plain\r\ncontent-length: 9\r\nconnection: close\r\n\r\nnot found"
286 .to_string(),
287 };
288 let _ = out.write_all(head.as_bytes());
289 if let (Some((_, bytes)), true) = (found, method == "GET") {
290 let _ = out.write_all(bytes);
291 }
292 let _ = out.flush();
293 let _ = conn.shutdown(Shutdown::Both);
294}
295
296static UNNAMED: AtomicU64 = AtomicU64::new(1);
298
299fn relay(mut conn: TcpStream, key: &str, token: &str, hub: &RelayHub) {
302 let accept = {
303 let digest = logicaffeine_base::hash::sha1(
304 format!("{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11").as_bytes(),
305 );
306 base64::engine::general_purpose::STANDARD.encode(digest)
307 };
308 let handshake = format!(
309 "HTTP/1.1 101 Switching Protocols\r\nupgrade: websocket\r\nconnection: Upgrade\r\nsec-websocket-accept: {accept}\r\n\r\n"
310 );
311 if conn.write_all(handshake.as_bytes()).is_err() {
312 return;
313 }
314 let _ = conn.set_read_timeout(None);
315 let Ok(writer) = conn.try_clone() else { return };
316 hub.join(token, writer);
317 let mut message = Vec::new();
318 loop {
319 let Some((fin, opcode, payload)) = read_frame(&mut conn) else {
320 break;
321 };
322 match opcode {
323 0x8 => break,
324 0x9 => {
325 let mut pong = vec![0x8A, payload.len() as u8];
326 pong.extend_from_slice(&payload);
327 let _ = conn.write_all(&pong);
328 }
329 0x0 | 0x1 | 0x2 => {
330 message.extend_from_slice(&payload);
331 if fin {
332 hub.receive(token, &message);
333 message.clear();
334 }
335 }
336 _ => {}
337 }
338 }
339 hub.leave(token);
340 let _ = conn.shutdown(Shutdown::Both);
341}
342
343fn read_frame(conn: &mut TcpStream) -> Option<(bool, u8, Vec<u8>)> {
346 let mut head = [0u8; 2];
347 conn.read_exact(&mut head).ok()?;
348 let fin = head[0] & 0x80 != 0;
349 let opcode = head[0] & 0x0F;
350 let masked = head[1] & 0x80 != 0;
351 let len = match head[1] & 0x7F {
352 126 => {
353 let mut b = [0u8; 2];
354 conn.read_exact(&mut b).ok()?;
355 u16::from_be_bytes(b) as usize
356 }
357 127 => {
358 let mut b = [0u8; 8];
359 conn.read_exact(&mut b).ok()?;
360 u64::from_be_bytes(b) as usize
361 }
362 n => n as usize,
363 };
364 let mut mask = [0u8; 4];
365 if masked {
366 conn.read_exact(&mut mask).ok()?;
367 }
368 let mut payload = vec![0u8; len];
369 conn.read_exact(&mut payload).ok()?;
370 if masked {
371 for (i, b) in payload.iter_mut().enumerate() {
372 *b ^= mask[i % 4];
373 }
374 }
375 Some((fin, opcode, payload))
376}