use crate::client::WebTorrent;
use crate::error::{Result, WebTorrentError};
use crate::torrent::Torrent;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tracing::{debug, error, info, warn};
use hex;
pub struct Server {
#[allow(dead_code)]
client: Arc<WebTorrent>,
destroyed: Arc<RwLock<bool>>,
#[allow(dead_code)]
handle: Option<tokio::task::JoinHandle<()>>,
port: u16,
}
impl Server {
pub async fn new(client: Arc<WebTorrent>, port: u16) -> Result<Self> {
let destroyed = Arc::new(RwLock::new(false));
let destroyed_clone = Arc::clone(&destroyed);
let client_clone = Arc::clone(&client);
let handle = tokio::task::spawn(async move {
let addr = format!("0.0.0.0:{}", port);
match TcpListener::bind(&addr).await {
Ok(listener) => {
info!("HTTP server listening on {}", addr);
loop {
if *destroyed_clone.read().await {
break;
}
match listener.accept().await {
Ok((stream, addr)) => {
debug!("HTTP connection from {}", addr);
let client_task = Arc::clone(&client_clone);
tokio::task::spawn(async move {
if let Err(e) = Self::handle_http_request(client_task, stream).await {
error!("Error handling HTTP request from {}: {}", addr, e);
}
});
}
Err(e) => {
if *destroyed_clone.read().await {
break;
}
error!("Error accepting HTTP connection: {}", e);
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
}
}
}
Err(e) => {
error!("Failed to bind HTTP server on {}: {}", addr, e);
}
}
});
Ok(Self {
client,
destroyed,
handle: Some(handle),
port,
})
}
async fn handle_http_request(
client: Arc<WebTorrent>,
mut stream: TcpStream,
) -> Result<()> {
let mut buffer = [0u8; 8192];
let n = stream.read(&mut buffer).await.map_err(|e| {
WebTorrentError::Network(format!("Failed to read HTTP request: {}", e))
})?;
if n == 0 {
return Ok(()); }
let request = String::from_utf8_lossy(&buffer[..n]);
let lines: Vec<&str> = request.lines().collect();
if lines.is_empty() {
return Ok(());
}
let request_line = lines[0];
let parts: Vec<&str> = request_line.split_whitespace().collect();
if parts.len() < 2 {
return Self::send_error_response(&mut stream, 400, "Bad Request").await;
}
let method = parts[0];
let path = parts[1];
if method != "GET" && method != "HEAD" {
return Self::send_error_response(&mut stream, 405, "Method Not Allowed").await;
}
let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
if path_parts.is_empty() || path_parts[0].is_empty() {
return Self::send_error_response(&mut stream, 404, "Not Found").await;
}
let info_hash_str = path_parts[0];
let info_hash = hex::decode(info_hash_str)
.map_err(|_| WebTorrentError::InvalidInfoHash("Invalid hex encoding".to_string()))?;
if info_hash.len() != 20 {
return Self::send_error_response(&mut stream, 400, "Invalid info hash length").await;
}
let mut hash = [0u8; 20];
hash.copy_from_slice(&info_hash);
let torrent = if let Some(t) = client.get(&hash).await {
t
} else {
return Self::send_error_response(&mut stream, 404, "Torrent not found").await;
};
if path_parts.len() > 1 {
let file_index: usize = path_parts[1].parse().map_err(|_| {
WebTorrentError::InvalidTorrent("Invalid file index".to_string())
})?;
let files = torrent.files();
if file_index >= files.len() {
return Self::send_error_response(&mut stream, 404, "File not found").await;
}
let _file = &files[file_index];
Self::serve_file(&mut stream, torrent.clone(), file_index, method == "HEAD").await?;
} else {
Self::serve_torrent_info(&mut stream, torrent.clone(), method == "HEAD").await?;
}
Ok(())
}
async fn serve_torrent_info(
stream: &mut TcpStream,
torrent: Arc<Torrent>,
is_head: bool,
) -> Result<()> {
let name = torrent.name();
let files = torrent.files();
let mut html = format!(
"<!DOCTYPE html><html><head><title>{}</title></head><body>",
html_escape::encode_text(name)
);
html.push_str(&format!("<h1>{}</h1>", html_escape::encode_text(name)));
html.push_str("<ul>");
for (i, file) in files.iter().enumerate() {
let file_name = file.path();
html.push_str(&format!(
"<li><a href=\"/{}/{}\">{}</a> ({})</li>",
hex::encode(torrent.info_hash()),
i,
html_escape::encode_text(file_name),
file.length()
));
}
html.push_str("</ul></body></html>");
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: text/html; charset=utf-8\r\n\
Content-Length: {}\r\n\
\r\n",
html.len()
);
stream.write_all(response.as_bytes()).await.map_err(|e| {
WebTorrentError::Network(format!("Failed to write HTTP response: {}", e))
})?;
if !is_head {
stream.write_all(html.as_bytes()).await.map_err(|e| {
WebTorrentError::Network(format!("Failed to write HTML: {}", e))
})?;
}
Ok(())
}
async fn serve_file(
stream: &mut TcpStream,
torrent: Arc<Torrent>,
file_index: usize,
is_head: bool,
) -> Result<()> {
let files = torrent.files();
if file_index >= files.len() {
return Self::send_error_response(stream, 404, "File not found").await;
}
let file = &files[file_index];
let file_name = file.path();
let _file_length = file.length();
let file_length = file.length();
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: application/octet-stream\r\n\
Content-Length: {}\r\n\
Content-Disposition: attachment; filename=\"{}\"\r\n\
\r\n",
file_length,
file_name
);
stream.write_all(response.as_bytes()).await.map_err(|e| {
WebTorrentError::Network(format!("Failed to write HTTP response: {}", e))
})?;
if !is_head {
warn!("File serving not fully implemented - would stream {} bytes", file_length);
}
Ok(())
}
async fn send_error_response(
stream: &mut TcpStream,
status_code: u16,
message: &str,
) -> Result<()> {
let response = format!(
"HTTP/1.1 {} {}\r\n\
Content-Type: text/plain\r\n\
Content-Length: {}\r\n\
\r\n\
{}",
status_code,
message,
message.len(),
message
);
stream.write_all(response.as_bytes()).await.map_err(|e| {
WebTorrentError::Network(format!("Failed to write error response: {}", e))
})?;
Ok(())
}
pub fn port(&self) -> u16 {
self.port
}
pub async fn destroy(&self) -> Result<()> {
if *self.destroyed.read().await {
return Ok(());
}
*self.destroyed.write().await = true;
Ok(())
}
}
mod html_escape {
pub fn encode_text(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
}