Vectrum
Sign In
263 lines · 10.0 KB · Rust
//! The host side: a WebSocket listener on its own threads. Each connection
//! proves it has the key, then trades JSON messages with the workspace,
//! which answers on the UI thread through [`ServerEvent`]s.

use std::{
    collections::HashMap,
    io::ErrorKind,
    net::{TcpListener, TcpStream},
    sync::{
        Arc, Mutex,
        atomic::{AtomicBool, AtomicU64, Ordering},
        mpsc,
    },
    thread,
    time::{Duration, Instant},
};

use tungstenite::{Message, WebSocket};

use super::{ClientMessage, PROTOCOL, Route, ServerMessage, keys_match, local_routes};

/// How long a connection may take to say hello.
const HELLO_TIMEOUT: Duration = Duration::from_secs(10);
/// How often a connection's thread looks for messages to send.
const POLL: Duration = Duration::from_millis(40);
/// How often the listener looks for changed addresses.
const RESCAN: Duration = Duration::from_secs(30);
/// A client that pings and then goes quiet this long went away, as when its
/// network did.
const SILENCE: Duration = Duration::from_secs(60);

pub enum ServerEvent {
    Connected { conn: u64, name: String, address: String },
    Message { conn: u64, message: ClientMessage },
    Disconnected { conn: u64 },
}

/// A client that's connected, for the settings to list.
#[derive(Clone, Debug)]
pub struct Peer {
    pub conn: u64,
    pub name: String,
    pub address: String,
}

pub struct RemoteServer {
    pub port: u16,
    stop: Arc<AtomicBool>,
    outboxes: Arc<Mutex<HashMap<u64, mpsc::Sender<String>>>>,
    /// Where this device listens now.
    routes: Arc<Mutex<Vec<Route>>>,
    pub events: smol::channel::Receiver<ServerEvent>,
}

impl RemoteServer {
    /// Starts listening on every interface. Connections must say `key`.
    pub fn start(port: u16, key: String, host: String) -> std::io::Result<Self> {
        let listener = TcpListener::bind(("0.0.0.0", port))?;
        listener.set_nonblocking(true)?;
        let stop = Arc::new(AtomicBool::new(false));
        let outboxes: Arc<Mutex<HashMap<u64, mpsc::Sender<String>>>> = Arc::default();
        let (events_tx, events) = smol::channel::unbounded();
        let next_conn = Arc::new(AtomicU64::new(1));
        let routes = Arc::new(Mutex::new(local_routes(port)));
        {
            let stop = stop.clone();
            let outboxes = outboxes.clone();
            let routes = routes.clone();
            thread::Builder::new().name("remote-listener".into()).spawn(move || {
                let mut scanned = Instant::now();
                while !stop.load(Ordering::Relaxed) {
                    if scanned.elapsed() >= RESCAN {
                        scanned = Instant::now();
                        let now = local_routes(port);
                        let changed = {
                            let mut routes = routes.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
                            let changed = *routes != now;
                            *routes = now.clone();
                            changed
                        };
                        if changed {
                            broadcast_to(&outboxes, &ServerMessage::Routes { routes: now });
                        }
                    }
                    match listener.accept() {
                        Ok((stream, address)) => {
                            let conn = next_conn.fetch_add(1, Ordering::Relaxed);
                            let (stop, outboxes, events, key, host, routes) =
                                (stop.clone(), outboxes.clone(), events_tx.clone(), key.clone(), host.clone(), routes.clone());
                            let _ = thread::Builder::new().name("remote-connection".into()).spawn(move || {
                                let routes = routes.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).clone();
                                serve(conn, stream, address.ip().to_string(), &key, &host, routes, &stop, &outboxes, &events);
                                outboxes.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).remove(&conn);
                            });
                        }
                        Err(err) if err.kind() == ErrorKind::WouldBlock => thread::sleep(Duration::from_millis(150)),
                        Err(_) => thread::sleep(Duration::from_millis(500)),
                    }
                }
            })?;
        }
        Ok(Self { port, stop, outboxes, routes, events })
    }

    /// Where this device listens now, LAN first.
    pub fn routes(&self) -> Vec<Route> {
        self.routes.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).clone()
    }

    pub fn send(&self, conn: u64, message: &ServerMessage) {
        self.sender(conn).send(message);
    }

    /// Sends to one client from any thread, for as long as it's connected.
    pub fn sender(&self, conn: u64) -> ConnectionSender {
        ConnectionSender { conn, outboxes: self.outboxes.clone() }
    }

    pub fn broadcast(&self, message: &ServerMessage) {
        broadcast_to(&self.outboxes, message);
    }

    /// Drops a client's connection.
    pub fn disconnect(&self, conn: u64) {
        // Its thread ends once its outbox is gone.
        self.outboxes.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).remove(&conn);
    }
}

fn broadcast_to(outboxes: &Mutex<HashMap<u64, mpsc::Sender<String>>>, message: &ServerMessage) {
    let Ok(text) = serde_json::to_string(message) else { return };
    for outbox in outboxes.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).values() {
        let _ = outbox.send(text.clone());
    }
}

impl Drop for RemoteServer {
    fn drop(&mut self) {
        self.stop.store(true, Ordering::Relaxed);
    }
}

/// A way to one client's connection that works off the UI thread.
#[derive(Clone)]
pub struct ConnectionSender {
    conn: u64,
    outboxes: Arc<Mutex<HashMap<u64, mpsc::Sender<String>>>>,
}

impl ConnectionSender {
    pub fn send(&self, message: &ServerMessage) {
        let Ok(text) = serde_json::to_string(message) else { return };
        if let Some(outbox) = self.outboxes.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).get(&self.conn) {
            let _ = outbox.send(text);
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn serve(
    conn: u64,
    stream: TcpStream,
    address: String,
    key: &str,
    host: &str,
    routes: Vec<Route>,
    stop: &AtomicBool,
    outboxes: &Mutex<HashMap<u64, mpsc::Sender<String>>>,
    events: &smol::channel::Sender<ServerEvent>,
) {
    let _ = stream.set_nonblocking(false);
    let _ = stream.set_nodelay(true);
    let _ = stream.set_read_timeout(Some(HELLO_TIMEOUT));
    // A client's Device panel streaming one of this device's simulators.
    if super::device_proxy::is_device_request(&stream) {
        super::device_proxy::serve(stream, key, || crate::device::service().hub_for_remote());
        return;
    }
    let Ok(mut socket) = tungstenite::accept(stream) else { return };

    let name = match read_message(&mut socket) {
        Some(ClientMessage::Hello { key: offered, name, protocol }) if keys_match(&offered, key) && protocol == PROTOCOL => name,
        Some(ClientMessage::Hello { protocol, .. }) if protocol != PROTOCOL => {
            deny(&mut socket, "This app's version speaks a different protocol; update both devices.");
            return;
        }
        _ => {
            // Slow down anyone guessing keys.
            thread::sleep(Duration::from_secs(1));
            deny(&mut socket, "Wrong key.");
            return;
        }
    };
    let welcome = ServerMessage::Welcome {
        host: host.to_string(),
        version: env!("CARGO_PKG_VERSION").into(),
        protocol: PROTOCOL,
        routes,
        updatable: crate::update::Updater::can_install(),
    };
    if socket.send(Message::text(serde_json::to_string(&welcome).unwrap_or_default())).is_err() {
        return;
    }
    let (outbox, inbox) = mpsc::channel::<String>();
    outboxes.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).insert(conn, outbox);
    let _ = events.send_blocking(ServerEvent::Connected { conn, name, address });
    let _ = socket.get_mut().set_read_timeout(Some(POLL));
    // Clients that ping are dropped once they go quiet; older ones don't ping.
    let mut pinged = false;
    let mut heard = Instant::now();

    'connection: while !stop.load(Ordering::Relaxed) {
        if pinged && heard.elapsed() > SILENCE {
            break;
        }
        loop {
            match inbox.try_recv() {
                Ok(text) => {
                    if socket.send(Message::text(text)).is_err() {
                        break 'connection;
                    }
                }
                Err(mpsc::TryRecvError::Empty) => break,
                Err(mpsc::TryRecvError::Disconnected) => break 'connection,
            }
        }
        let read = socket.read();
        if read.is_ok() {
            heard = Instant::now();
        }
        match read {
            Ok(Message::Text(text)) => {
                if let Ok(message) = serde_json::from_str::<ClientMessage>(text.as_str()) {
                    let _ = events.send_blocking(ServerEvent::Message { conn, message });
                }
            }
            Ok(Message::Close(_)) => break,
            // Its pong goes out with the next read or send.
            Ok(Message::Ping(_)) => pinged = true,
            Ok(_) => {}
            Err(tungstenite::Error::Io(err)) if matches!(err.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
            Err(_) => break,
        }
    }
    let _ = socket.close(None);
    let _ = events.send_blocking(ServerEvent::Disconnected { conn });
}

fn read_message(socket: &mut WebSocket<TcpStream>) -> Option<ClientMessage> {
    loop {
        match socket.read().ok()? {
            Message::Text(text) => return serde_json::from_str(text.as_str()).ok(),
            Message::Ping(_) | Message::Pong(_) => continue,
            _ => return None,
        }
    }
}

fn deny(socket: &mut WebSocket<TcpStream>, reason: &str) {
    let denied = ServerMessage::Denied { reason: reason.into() };
    let _ = socket.send(Message::text(serde_json::to_string(&denied).unwrap_or_default()));
    let _ = socket.close(None);
}