1use std::collections::{HashMap, VecDeque};
14use std::io::ErrorKind;
15use std::sync::atomic::{AtomicU32, Ordering};
16use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError};
17use std::sync::Arc;
18use std::time::Duration;
19
20use tungstenite::stream::MaybeTlsStream;
21use tungstenite::{Message, WebSocket};
22
23pub const CONNECTING: u32 = 0;
24pub const OPEN: u32 = 1;
25pub const CLOSED: u32 = 2;
26pub const ERROR: u32 = 3;
27
28pub const MAX_CONNECTIONS: usize = 16;
29const QUEUE: usize = 4096;
31const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
32
33struct Conn {
34 state: Arc<AtomicU32>,
35 outgoing: SyncSender<Vec<u8>>,
36 thread: Option<std::thread::JoinHandle<()>>,
37 incoming: Receiver<Vec<u8>>,
38 queue: VecDeque<Vec<u8>>,
39}
40
41#[derive(Clone, Debug, Default)]
45pub struct NetPolicy {
46 pub allowed: bool,
47 pub hosts: Vec<String>,
49 pub consent: Option<crate::consent::Consent>,
50}
51
52impl NetPolicy {
53 pub fn new(allowed: bool, hosts: Vec<String>) -> NetPolicy {
54 NetPolicy { allowed, hosts: hosts.into_iter().map(|h| h.to_ascii_lowercase()).collect(), consent: None }
55 }
56
57 pub fn with_consent(mut self, consent: Option<crate::consent::Consent>) -> NetPolicy {
58 self.consent = consent;
59 self
60 }
61
62 pub fn verdict(&self, url: &str) -> Result<Option<String>, ()> {
65 let Some(why) = self.refusal(url) else { return Ok(None) };
66 let Some(c) = &self.consent else { return Ok(Some(why)) };
67 let host = url_host(url).unwrap_or_default();
68 match c.lock().unwrap().check(&crate::consent::net_subject(&host)) {
69 Some(true) => Ok(None),
70 Some(false) => Ok(Some(format!("the player said no to {host}"))),
71 None => Err(()),
72 }
73 }
74
75 pub fn wait(&self, url: &str, cancelled: impl Fn() -> bool) -> Result<(), String> {
78 loop {
79 match self.verdict(url) {
80 Ok(None) => return Ok(()),
81 Ok(Some(why)) => return Err(why),
82 Err(()) if cancelled() => return Err("cancelled".into()),
83 Err(()) => std::thread::sleep(Duration::from_millis(50)),
84 }
85 }
86 }
87
88 pub fn permits(&self, host: &str) -> bool {
89 if !self.allowed {
90 return false;
91 }
92 let host = host.trim_end_matches('.').to_ascii_lowercase();
93 self.hosts.is_empty()
94 || self.hosts.iter().any(|p| match p.strip_prefix("*.") {
95 Some(domain) => host.len() > domain.len() + 1 && host.ends_with(domain) && host.as_bytes()[host.len() - domain.len() - 1] == b'.',
96 None => host == *p,
97 })
98 }
99
100 pub fn refusal(&self, url: &str) -> Option<String> {
102 if !self.allowed {
103 return Some("run with --allow-net".into());
104 }
105 let host = url_host(url).unwrap_or_default();
106 (!self.permits(&host)).then(|| format!("{host} is not in --allow-net={}", self.hosts.join(",")))
107 }
108}
109
110pub fn url_host(url: &str) -> Option<String> {
112 let rest = url.split_once("://")?.1;
113 let authority = rest.split(['/', '?', '#']).next()?;
114 let authority = authority.rsplit_once('@').map_or(authority, |(_, a)| a);
115 let host = if authority.starts_with('[') {
116 &authority[..=authority.find(']')?]
117 } else {
118 authority.split(':').next()?
119 };
120 (!host.is_empty()).then(|| host.to_ascii_lowercase())
121}
122
123pub struct Net {
124 policy: NetPolicy,
125 conns: HashMap<i32, Conn>,
126 next: i32,
127}
128
129impl Net {
130 pub fn new(allowed: bool) -> Net {
131 Net::with_policy(NetPolicy::new(allowed, Vec::new()))
132 }
133
134 pub fn with_policy(policy: NetPolicy) -> Net {
135 Net { policy, conns: HashMap::new(), next: 1 }
136 }
137
138 pub fn open(&mut self, url: &str) -> i32 {
139 if let Ok(Some(why)) = self.policy.verdict(url) {
141 eprintln!("[gasm] net: denied connection to {url} ({why})");
142 return -1;
143 }
144 if !url.starts_with("ws://") && !url.starts_with("wss://") {
145 eprintln!("[gasm] net: only ws:// and wss:// URLs are supported: {url}");
146 return -1;
147 }
148 if self.conns.len() >= MAX_CONNECTIONS {
149 eprintln!("[gasm] net: too many connections (max {MAX_CONNECTIONS}): {url}");
150 return -1;
151 }
152 install_crypto_provider();
153 let state = Arc::new(AtomicU32::new(CONNECTING));
154 let (out_tx, out_rx) = mpsc::sync_channel::<Vec<u8>>(QUEUE);
155 let (in_tx, in_rx) = mpsc::sync_channel::<Vec<u8>>(QUEUE);
156 let (st, url_owned, policy) = (state.clone(), url.to_owned(), self.policy.clone());
157 let thread = std::thread::spawn(move || {
158 let gone = || matches!(out_rx.try_recv(), Err(TryRecvError::Disconnected));
160 if let Err(why) = policy.wait(&url_owned, gone) {
161 eprintln!("[gasm] net: denied connection to {url_owned} ({why})");
162 st.store(ERROR, Ordering::Release);
163 return;
164 }
165 run(&url_owned, &st, out_rx, in_tx)
166 });
167 let h = self.next;
168 self.next += 1;
169 self.conns.insert(h, Conn { state, outgoing: out_tx, incoming: in_rx, queue: VecDeque::new(), thread: Some(thread) });
170 h
171 }
172
173 fn check(&self, h: i32) -> Result<bool, String> {
176 if h <= 0 || h >= self.next {
177 return Err(format!("gasm:net: invalid connection handle {h}"));
178 }
179 Ok(self.conns.contains_key(&h))
180 }
181
182 pub fn state(&self, h: i32) -> Result<u32, String> {
183 Ok(match self.check(h)? {
184 true => self.conns[&h].state.load(Ordering::Acquire),
185 false => CLOSED,
186 })
187 }
188
189 pub fn send(&mut self, h: i32, msg: Vec<u8>) -> Result<i32, String> {
191 if !self.check(h)? {
192 return Ok(-1);
193 }
194 let c = &self.conns[&h];
195 if c.state.load(Ordering::Acquire) != OPEN {
196 return Ok(-1);
197 }
198 Ok(match c.outgoing.try_send(msg) {
199 Ok(()) => 0,
200 Err(TrySendError::Full(_)) => {
201 eprintln!("[gasm] net: send queue full ({QUEUE} messages)");
202 -1
203 }
204 Err(TrySendError::Disconnected(_)) => -1,
205 })
206 }
207
208 pub fn peek(&mut self, h: i32) -> Result<Result<&[u8], i32>, String> {
210 if !self.check(h)? {
211 return Ok(Err(-1));
212 }
213 Ok(self.peek_open(h))
214 }
215
216 fn peek_open(&mut self, h: i32) -> Result<&[u8], i32> {
217 let Some(c) = self.conns.get_mut(&h) else { return Err(-1) };
218 while let Ok(m) = c.incoming.try_recv() {
219 c.queue.push_back(m);
220 }
221 if c.queue.is_empty() {
222 return Err(if c.state.load(Ordering::Acquire) >= CLOSED { -1 } else { 0 });
223 }
224 Ok(c.queue.front().unwrap())
225 }
226
227 pub fn pop(&mut self, h: i32) {
228 if let Some(c) = self.conns.get_mut(&h) {
229 c.queue.pop_front();
230 }
231 }
232
233 pub fn close(&mut self, h: i32) -> Result<(), String> {
235 self.check(h)?;
236 self.conns.remove(&h); Ok(())
238 }
239}
240
241impl Drop for Net {
242 fn drop(&mut self) {
245 let threads: Vec<_> = self.conns.drain().filter_map(|(_, mut c)| c.thread.take()).collect();
246 let deadline = std::time::Instant::now() + Duration::from_secs(2);
247 for t in threads {
248 while !t.is_finished() && std::time::Instant::now() < deadline {
249 std::thread::sleep(Duration::from_millis(5));
250 }
251 }
252 }
253}
254
255fn run(url: &str, state: &AtomicU32, outgoing: Receiver<Vec<u8>>, incoming: SyncSender<Vec<u8>>) {
258 let mut ws = match connect(url) {
259 Ok(ws) => ws,
260 Err(e) => {
261 eprintln!("[gasm] net: {url}: {e}");
262 state.store(ERROR, Ordering::Release);
263 return;
264 }
265 };
266 let tcp = match ws.get_mut() {
270 MaybeTlsStream::Plain(s) => Some(&*s),
271 MaybeTlsStream::Rustls(s) => Some(&s.sock),
272 _ => None,
273 };
274 if let Some(tcp) = tcp {
275 let _ = tcp.set_nodelay(true);
276 if let Err(e) = tcp.set_nonblocking(true) {
277 eprintln!("[gasm] net: {url}: {e}");
278 state.store(ERROR, Ordering::Release);
279 return;
280 }
281 }
282 state.store(OPEN, Ordering::Release);
283 let end = pump(&mut ws, &outgoing, &incoming);
284 if let Err(e) = &end {
285 eprintln!("[gasm] net: {url}: {e}");
286 }
287 state.store(if end.is_ok() { CLOSED } else { ERROR }, Ordering::Release);
288 close_gracefully(&mut ws);
289}
290
291fn connect(url: &str) -> Result<WebSocket<MaybeTlsStream<std::net::TcpStream>>, String> {
293 use std::net::ToSocketAddrs;
294 let uri: tungstenite::http::Uri = url.parse().map_err(|e| format!("bad URL: {e}"))?;
295 let host = uri.host().ok_or("URL has no host")?.trim_start_matches('[').trim_end_matches(']');
296 let port = uri.port_u16().unwrap_or(if uri.scheme_str() == Some("wss") { 443 } else { 80 });
297 let mut last = String::from("no address");
298 for addr in (host, port).to_socket_addrs().map_err(|e| e.to_string())? {
299 match std::net::TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {
300 Ok(tcp) => {
301 tcp.set_read_timeout(Some(CONNECT_TIMEOUT)).map_err(|e| e.to_string())?;
302 tcp.set_write_timeout(Some(CONNECT_TIMEOUT)).map_err(|e| e.to_string())?;
303 let (ws, _) = tungstenite::client_tls(url, tcp).map_err(|e| e.to_string())?;
304 let tcp = match ws.get_ref() {
305 MaybeTlsStream::Plain(s) => s,
306 MaybeTlsStream::Rustls(s) => &s.sock,
307 _ => return Ok(ws),
308 };
309 tcp.set_read_timeout(None).map_err(|e| e.to_string())?;
310 tcp.set_write_timeout(None).map_err(|e| e.to_string())?;
311 return Ok(ws);
312 }
313 Err(e) => last = format!("{addr}: {e}"),
314 }
315 }
316 Err(last)
317}
318
319fn close_gracefully(ws: &mut WebSocket<MaybeTlsStream<std::net::TcpStream>>) {
323 if ws.close(None).is_err() {
324 return;
325 }
326 let deadline = std::time::Instant::now() + Duration::from_secs(1);
327 while std::time::Instant::now() < deadline {
328 match ws.read() {
329 Ok(_) => {}
330 Err(e) if would_block(&e) => std::thread::sleep(Duration::from_millis(1)),
331 Err(_) => break, }
333 }
334}
335
336pub fn install_crypto_provider() {
338 static ONCE: std::sync::Once = std::sync::Once::new();
339 ONCE.call_once(|| {
340 let _ = rustls::crypto::ring::default_provider().install_default();
341 });
342}
343
344fn would_block(e: &tungstenite::Error) -> bool {
345 matches!(e, tungstenite::Error::Io(e) if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut | ErrorKind::Interrupted))
346}
347
348pub(crate) fn pump(
352 ws: &mut WebSocket<MaybeTlsStream<std::net::TcpStream>>,
353 outgoing: &Receiver<Vec<u8>>,
354 incoming: &SyncSender<Vec<u8>>,
355) -> Result<(), String> {
356 let mut pending: Option<Vec<u8>> = None;
358 loop {
359 let mut busy = false;
360 loop {
361 match outgoing.try_recv() {
362 Ok(m) => {
363 busy = true;
364 match ws.send(Message::Binary(m.into())) {
365 Ok(()) => {}
366 Err(e) if would_block(&e) => {}
367 Err(e) => return Err(e.to_string()),
368 }
369 }
370 Err(TryRecvError::Empty) => break,
371 Err(TryRecvError::Disconnected) => {
372 flush_all(ws);
373 return Ok(()); }
375 }
376 }
377 match ws.flush() {
378 Ok(()) => {}
379 Err(e) if would_block(&e) => {}
380 Err(e) => return Err(e.to_string()),
381 }
382 if let Some(m) = pending.take() {
383 match incoming.try_send(m) {
384 Ok(()) => busy = true,
385 Err(TrySendError::Full(m)) => pending = Some(m),
386 Err(TrySendError::Disconnected(_)) => return Ok(()),
387 }
388 }
389 let read = if pending.is_some() { Err(tungstenite::Error::Io(ErrorKind::WouldBlock.into())) } else { ws.read() };
390 match read {
391 Ok(Message::Binary(b)) => {
392 busy = true;
393 pending = Some(b.to_vec());
394 }
395 Ok(Message::Text(t)) => {
396 busy = true;
397 pending = Some(t.as_bytes().to_vec());
398 }
399 Ok(Message::Close(_)) => return Ok(()),
400 Ok(_) => busy = true,
401 Err(e) if would_block(&e) => {}
402 Err(tungstenite::Error::ConnectionClosed | tungstenite::Error::AlreadyClosed) => return Ok(()),
403 Err(e) => return Err(e.to_string()),
404 }
405 if !busy {
406 std::thread::sleep(Duration::from_millis(1));
407 }
408 }
409}
410
411fn flush_all(ws: &mut WebSocket<MaybeTlsStream<std::net::TcpStream>>) {
413 let deadline = std::time::Instant::now() + Duration::from_secs(1);
414 while std::time::Instant::now() < deadline {
415 match ws.flush() {
416 Err(e) if would_block(&e) => std::thread::sleep(Duration::from_millis(1)),
417 _ => return,
418 }
419 }
420}
421
422#[cfg(test)]
423mod policy_tests {
424 use super::*;
425
426 #[test]
427 fn hosts() {
428 assert_eq!(url_host("https://User@API.met.no:443/x?y").as_deref(), Some("api.met.no"));
429 assert_eq!(url_host("http://[::1]:8080/").as_deref(), Some("[::1]"));
430 assert_eq!(url_host("ws://localhost").as_deref(), Some("localhost"));
431 let p = NetPolicy::new(true, vec!["api.met.no".into(), "*.example.org".into()]);
432 assert!(p.permits("api.met.no") && p.permits("API.MET.NO.") && p.permits("a.b.example.org"));
433 assert!(!p.permits("met.no") && !p.permits("example.org") && !p.permits("badexample.org"));
434 assert!(NetPolicy::new(true, vec![]).permits("anything"));
435 assert!(!NetPolicy::new(false, vec![]).permits("anything"));
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 fn settle(net: &Net, h: i32) -> u32 {
445 let t0 = std::time::Instant::now();
446 while net.state(h).unwrap() == CONNECTING && t0.elapsed() < Duration::from_secs(10) {
447 std::thread::sleep(Duration::from_millis(20));
448 }
449 net.state(h).unwrap()
450 }
451
452 #[test]
454 #[ignore = "needs internet"]
455 fn wss_public_echo() {
456 let mut net = Net::new(true);
457 let h = net.open("wss://echo.websocket.org");
458 assert!(h > 0);
459 assert_eq!(settle(&net, h), OPEN, "TLS connection did not open");
460 assert_eq!(net.send(h, b"gasm-tls-check".to_vec()), Ok(0));
461 let t0 = std::time::Instant::now();
462 let mut echoed = false;
463 while t0.elapsed() < Duration::from_secs(10) && !echoed {
464 match net.peek(h).unwrap() {
465 Ok(m) => {
466 echoed = m == b"gasm-tls-check";
467 net.pop(h);
468 }
469 Err(0) => std::thread::sleep(Duration::from_millis(20)),
470 Err(_) => break,
471 }
472 }
473 assert!(echoed, "no echo over wss://");
474 }
475
476 #[test]
477 fn denied_without_permission() {
478 assert_eq!(Net::new(false).open("wss://example.com"), -1);
479 assert_eq!(Net::new(true).open("http://example.com"), -1);
480 }
481
482 #[test]
483 fn unknown_handles_are_errors_closed_ones_are_not() {
484 let mut net = Net::new(true);
485 assert!(net.state(1).is_err() && net.state(0).is_err() && net.close(7).is_err());
486 let h = net.open("ws://127.0.0.1:1"); assert!(h > 0);
488 net.close(h).unwrap();
489 assert_eq!(net.state(h), Ok(CLOSED));
490 assert_eq!(net.send(h, vec![1]), Ok(-1));
491 assert_eq!(net.peek(h).unwrap().err(), Some(-1));
492 net.close(h).unwrap(); }
494}