liminal-cli 0.3.32

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! Embedded HTTP files and the shared production WebSocket door at one origin.

use std::io::{self, Read as _, Write as _};
use std::net::{SocketAddr, TcpListener, TcpStream};

use liminal_server::config::WebSocketConfig;
use liminal_server::server::connection::{ConnectionSupervisor, WebSocketListener};
use tungstenite::handshake::server::{Request, create_response};

include!(concat!(env!("OUT_DIR"), "/console_assets.rs"));

pub(super) fn start(
    listener: TcpListener,
    supervisor: ConnectionSupervisor,
) -> Result<WebSocketListener, liminal_server::ServerError> {
    let listen =
        listener
            .local_addr()
            .map_err(|error| liminal_server::ServerError::ListenerAccept {
                message: format!("console.listen: {error}"),
            })?;
    let settings = settings(listen);
    WebSocketListener::accept_http_on(listener, &settings, supervisor, handle)
}

fn settings(listen_address: SocketAddr) -> WebSocketConfig {
    WebSocketConfig {
        listen_address,
        path: "/exchange".to_owned(),
        allowed_origins: vec![format!("http://{listen_address}")],
        allowed_origin_ports: vec![listen_address.port()],
        ping_interval_ms: None,
    }
}

pub(super) fn asset(path: &str) -> Option<(&'static str, &'static [u8])> {
    let path = if path == "/" { "/index.html" } else { path };
    EMBEDDED
        .iter()
        .find(|(name, _, _)| *name == path)
        .map(|(_, kind, bytes)| (*kind, *bytes))
}

fn handle(stream: &mut TcpStream) -> io::Result<bool> {
    let request = match request(stream) {
        Ok(request) => request,
        Err(error) => {
            answer(
                stream,
                "400 Bad Request",
                "text/plain; charset=utf-8",
                format!("HTTP request refused: {error}").as_bytes(),
            )?;
            return Ok(false);
        }
    };
    if request.method() != "GET" {
        answer(
            stream,
            "405 Method Not Allowed",
            "text/plain; charset=utf-8",
            b"only GET",
        )?;
    } else if request.uri().path() == "/exchange" {
        return upgrade(stream, &request);
    } else if request.uri().path() == "/console.json" {
        answer(
            stream,
            "200 OK",
            "application/json",
            b"{\"signin\":false,\"lys\":null}",
        )?;
    } else if let Some((kind, bytes)) = asset(request.uri().path()) {
        answer(stream, "200 OK", kind, bytes)?;
    } else {
        answer(
            stream,
            "404 Not Found",
            "text/plain; charset=utf-8",
            b"path is outside the embedded dist",
        )?;
    }
    Ok(false)
}

fn request(stream: &mut TcpStream) -> io::Result<Request> {
    // Reading exactly the head leaves any first protocol frame on its socket.
    // The bound applies before parsing, including a peer that never sends a newline.
    let mut head = Vec::new();
    while !head.ends_with(b"\r\n\r\n") {
        if head.len() == 16_384 {
            return Err(io::Error::other("HTTP request head exceeds 16384 bytes"));
        }
        let mut byte = [0_u8; 1];
        stream.read_exact(&mut byte)?;
        head.push(byte[0]);
    }
    let head = std::str::from_utf8(&head).map_err(io::Error::other)?;
    let mut lines = head.split("\r\n");
    let mut start = lines.next().unwrap_or_default().split_whitespace();
    let method = start
        .next()
        .ok_or_else(|| io::Error::other("method is absent"))?;
    let path = start
        .next()
        .ok_or_else(|| io::Error::other("path is absent"))?;
    if start.next() != Some("HTTP/1.1") || start.next().is_some() || !path.starts_with('/') {
        return Err(io::Error::other("expected an HTTP/1.1 origin request"));
    }
    let mut builder = Request::builder().method(method).uri(path);
    for line in lines.take_while(|line| !line.is_empty()) {
        let (name, value) = line
            .split_once(':')
            .ok_or_else(|| io::Error::other("malformed HTTP header"))?;
        builder = builder.header(name, value.trim());
    }
    builder.body(()).map_err(io::Error::other)
}

fn upgrade(stream: &mut TcpStream, request: &Request) -> io::Result<bool> {
    if let Some(origin) = request.headers().get("origin") {
        let host = request
            .headers()
            .get("host")
            .and_then(|host| host.to_str().ok());
        if host.is_none_or(|host| origin.as_bytes() != format!("http://{host}").as_bytes()) {
            answer(
                stream,
                "403 Forbidden",
                "text/plain; charset=utf-8",
                b"WebSocket origin must name this console's origin",
            )?;
            return Ok(false);
        }
    }
    let response = match create_response(request) {
        Ok(response) => response,
        Err(error) => {
            answer(
                stream,
                "426 Upgrade Required",
                "text/plain; charset=utf-8",
                format!("WebSocket upgrade required: {error}").as_bytes(),
            )?;
            return Ok(false);
        }
    };
    write!(stream, "HTTP/1.1 101 Switching Protocols\r\n")?;
    for (name, value) in response.headers() {
        stream.write_all(name.as_str().as_bytes())?;
        stream.write_all(b": ")?;
        stream.write_all(value.as_bytes())?;
        stream.write_all(b"\r\n")?;
    }
    stream.write_all(b"\r\n")?;
    stream.flush()?;
    Ok(true)
}

fn answer(stream: &mut TcpStream, status: &str, kind: &str, bytes: &[u8]) -> io::Result<()> {
    write!(
        stream,
        "HTTP/1.1 {status}\r\nContent-Type: {kind}\r\nContent-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n",
        bytes.len()
    )?;
    stream.write_all(bytes)?;
    stream.flush()
}