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> {
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()
}