use crate::network::SessionToken;
use crate::relay::{self, SessionTable};
use crate::server::renderer::web;
use anyhow::{Context, Result};
use std::sync::atomic::AtomicU64;
use tokio::io::AsyncWriteExt;
pub(crate) async fn handle_http<S: web::ByteStream>(
mut stream: S,
sessions: &SessionTable,
next_gen: &AtomicU64,
token: &SessionToken,
) -> Result<()> {
let head = web::read_head(&mut stream).await?;
let path = head.path.clone();
let presented = head.query.get("token").map(String::as_str).unwrap_or("");
let authorized = crate::network::verify_token(token, presented);
match route(&path) {
Route::Script => {
web::respond(
&mut stream,
200,
"application/javascript; charset=utf-8",
web::CLIENT_JS.as_bytes(),
)
.await
}
Route::Page => {
web::respond(
&mut stream,
200,
"text/html; charset=utf-8",
web::INDEX_HTML.as_bytes(),
)
.await
}
Route::RedirectTo(location) => redirect(&mut stream, &location).await,
Route::Socket(session) => {
if !authorized {
return web::respond(
&mut stream,
401,
"text/plain; charset=utf-8",
b"Unauthorized: open the exact link your sharer printed, including #token=...",
)
.await;
}
if !relay::session_has_host(sessions, &session).await {
return web::respond(
&mut stream,
503,
"text/plain; charset=utf-8",
b"no host in this session yet -- is the sharer still running?",
)
.await;
}
let key = head
.ws_key
.clone()
.context("missing Sec-WebSocket-Key: this is not a WebSocket upgrade")?;
bridge(&mut stream, &key, sessions, next_gen, &session).await
}
Route::NotFound(why) => {
web::respond(
&mut stream,
404,
"text/plain; charset=utf-8",
why.as_bytes(),
)
.await
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum Route {
Page,
Script,
Socket(String),
RedirectTo(String),
NotFound(&'static str),
}
const NO_SESSION: &str = "no session in this URL; the sharer prints the exact link";
const NOT_FOUND: &str = "not found: a viewer URL looks like /v/<session>/#token=...";
fn route(path: &str) -> Route {
if path == "/pcc.js" {
return Route::Script;
}
let Some(rest) = path.strip_prefix("/v/") else {
return Route::NotFound(NOT_FOUND);
};
match rest.split_once('/') {
None if rest.is_empty() => Route::NotFound(NO_SESSION),
None => Route::RedirectTo(format!("/v/{rest}/")),
Some(("", _)) => Route::NotFound(NO_SESSION),
Some((_, "")) => Route::Page,
Some((session, "ws")) => Route::Socket(session.to_string()),
Some(_) => Route::NotFound(NOT_FOUND),
}
}
async fn redirect<S: web::ByteStream>(stream: &mut S, location: &str) -> Result<()> {
let head = format!(
"HTTP/1.1 301 Moved Permanently\r\nLocation: {location}\r\nContent-Length: 0\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n"
);
stream.write_all(head.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
async fn bridge<S: web::ByteStream>(
stream: &mut S,
key: &str,
sessions: &SessionTable,
next_gen: &AtomicU64,
session_id: &str,
) -> Result<()> {
let response = format!(
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {}\r\n\r\n",
web::ws_accept(key)
);
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
let mut viewer = match relay::register_browser_viewer(sessions, next_gen, session_id).await {
Ok(v) => v,
Err(e) => {
tracing::warn!("relay browser viewer refused: {e}");
let _ =
web::ws_write_frame(stream, 0x1, format!("{{\"error\":\"{e}\"}}").as_bytes()).await;
return Ok(());
}
};
let viewer_id = viewer.id;
tracing::info!("relay: browser viewer {viewer_id} joined session '{session_id}'");
loop {
tokio::select! {
frame = web::read_ws_frame(stream) => {
match frame? {
web::WsFrame::Close => break,
web::WsFrame::Ping(payload) => {
if web::ws_write_frame(stream, 0xA, &payload).await.is_err() {
break;
}
}
web::WsFrame::Other => {}
web::WsFrame::Binary(payload) => {
if relay::forward_browser_payload(sessions, session_id, viewer_id, &payload)
.await
.is_err()
{
break;
}
}
}
}
outbound = viewer.rx.recv() => {
let Some(framed) = outbound else { break };
if web::ws_write_frame(stream, 0x2, strip_len_prefix(&framed)).await.is_err() {
break;
}
}
}
}
relay::unregister_browser_viewer(sessions, session_id, &viewer).await;
tracing::info!("relay: browser viewer {viewer_id} left session '{session_id}'");
Ok(())
}
fn strip_len_prefix(framed: &[u8]) -> &[u8] {
if framed.len() >= 4 {
let len = u32::from_le_bytes([framed[0], framed[1], framed[2], framed[3]]) as usize;
if len + 4 == framed.len() {
return &framed[4..];
}
}
framed
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_urls_route_to_page_socket_or_redirect() {
assert_eq!(route("/v/ABC"), Route::RedirectTo("/v/ABC/".into()));
assert_eq!(route("/v/ABC/"), Route::Page);
assert_eq!(route("/v/ABC/ws"), Route::Socket("ABC".into()));
assert_eq!(route("/pcc.js"), Route::Script);
}
#[test]
fn a_session_url_without_a_session_is_not_a_page() {
assert_eq!(route("/v/"), Route::NotFound(NO_SESSION));
assert_eq!(route("/v//ws"), Route::NotFound(NO_SESSION));
assert_eq!(route("/"), Route::NotFound(NOT_FOUND));
assert_eq!(route("/v/ABC/nope"), Route::NotFound(NOT_FOUND));
}
#[test]
fn the_viewer_leg_prefix_is_stripped_for_a_websocket() {
let payload = b"hello";
let mut framed = (payload.len() as u32).to_le_bytes().to_vec();
framed.extend_from_slice(payload);
assert_eq!(strip_len_prefix(&framed), payload);
assert_eq!(strip_len_prefix(b"ab"), b"ab");
assert_eq!(
strip_len_prefix(b"\x09\x00\x00\x00ab"),
b"\x09\x00\x00\x00ab"
);
}
}