use std::path::{Path, PathBuf};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast;
const RELOAD_SCRIPT: &str =
r#""#;
pub async fn run(port: u16, html_path: PathBuf, reload: broadcast::Sender<()>) {
let listener = match TcpListener::bind(("127.0.0.1", port)).await {
Ok(l) => l,
Err(e) => {
eprintln!("error: failed to bind to port {}: {}", port, e);
return;
}
};
loop {
if let Ok((stream, _)) = listener.accept().await {
let path = html_path.clone();
let rx = reload.subscribe();
tokio::spawn(async move {
let _ = handle(stream, path, rx).await;
});
}
}
}
async fn handle(
mut stream: TcpStream,
html_path: PathBuf,
reload_rx: broadcast::Receiver<()>,
) -> std::io::Result<()> {
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).await?;
if n == 0 {
return Ok(());
}
let request = String::from_utf8_lossy(&buf[..n]);
let path = request
.lines()
.next()
.and_then(|line| line.split_whitespace().nth(1))
.unwrap_or("/");
if path == "/_events" {
handle_sse(stream, reload_rx).await
} else {
handle_file(stream, &html_path, path).await
}
}
async fn handle_sse(
mut stream: TcpStream,
mut rx: broadcast::Receiver<()>,
) -> std::io::Result<()> {
let headers = "HTTP/1.1 200 OK\r\n\
Content-Type: text/event-stream\r\n\
Cache-Control: no-cache\r\n\
Connection: keep-alive\r\n\r\n";
stream.write_all(headers.as_bytes()).await?;
loop {
match rx.recv().await {
Ok(()) => {
if stream.write_all(b"data: reload\n\n").await.is_err() {
break;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(broadcast::error::RecvError::Closed) => break,
}
}
Ok(())
}
async fn handle_file(
mut stream: TcpStream,
html_path: &Path,
request_path: &str,
) -> std::io::Result<()> {
if request_path.contains("..") {
stream
.write_all(b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n")
.await?;
return Ok(());
}
let file_path = if request_path == "/" {
html_path.to_path_buf()
} else {
let dir = html_path.parent().unwrap_or(Path::new("."));
dir.join(&request_path[1..])
};
let body = match std::fs::read(&file_path) {
Ok(content) => {
if file_path.extension().map_or(false, |e| e == "html") {
inject_reload_script(&content)
} else {
content
}
}
Err(_) => {
stream
.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")
.await?;
return Ok(());
}
};
let content_type = content_type_for(&file_path);
let header = format!(
"HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nCache-Control: no-cache\r\n\r\n",
body.len(),
);
stream.write_all(header.as_bytes()).await?;
stream.write_all(&body).await?;
Ok(())
}
fn inject_reload_script(content: &[u8]) -> Vec {
let html = String::from_utf8_lossy(content);
match html.rfind("