pub mod actions;
use anyhow::Result;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
use tracing::error;
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use crate::utils::WireFailure;
use actions::TorrentTrackerProtocol;
const REQUEST_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
static CONNECTION_CAP_REFUSAL: std::sync::LazyLock<Vec<u8>> = std::sync::LazyLock::new(|| {
let mut dict = HashMap::new();
dict.insert(
b"failure reason".to_vec(),
serde_bencode::value::Value::Bytes(b"too many connections, try again later".to_vec()),
);
let body =
serde_bencode::to_bytes(&serde_bencode::value::Value::Dict(dict)).unwrap_or_default();
let mut out = format!(
"HTTP/1.1 503 Service Unavailable\r\nRetry-After: 30\r\nContent-Type: \
text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
)
.into_bytes();
out.extend_from_slice(&body);
out
});
type SharedWrite = Arc<Mutex<tokio::io::WriteHalf<tokio::net::TcpStream>>>;
pub struct TorrentTrackerServer;
impl TorrentTrackerServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
) -> Result<SocketAddr> {
let listener = TcpListener::bind(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"BitTorrent Tracker server (action-based) listening on {}",
local_addr
));
let protocol = Arc::new(TorrentTrackerProtocol::new());
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
&CONNECTION_CAP_REFUSAL,
"BitTorrent Tracker",
Some(&status_tx),
)
.await
{
Ok((stream, peer_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let llm_clone = llm_client.clone();
let state_clone = app_state.clone();
let status_clone = status_tx.clone();
let protocol_clone = protocol.clone();
Log::new(Some(&status_clone)).debug(format!(
"BitTorrent Tracker accepted connection from {}",
peer_addr
));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr: peer_addr,
local_addr,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
state_clone
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_clone.send("__UPDATE_UI__".to_string());
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
if let Err(e) = Self::handle_connection(
stream,
peer_addr,
local_addr,
connection_id,
llm_clone,
state_clone,
status_clone,
server_id,
protocol_clone,
)
.await
{
error!("BitTorrent Tracker connection error: {}", e);
}
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("BitTorrent Tracker accept error: {}", e));
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
#[allow(clippy::too_many_arguments)]
async fn handle_connection(
stream: tokio::net::TcpStream,
peer_addr: SocketAddr,
_local_addr: SocketAddr,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
protocol: Arc<TorrentTrackerProtocol>,
) -> Result<()> {
use tokio::io::AsyncWriteExt;
let (read_half, write_half) = tokio::io::split(stream);
let write_half: SharedWrite = Arc::new(Mutex::new(write_half));
let peer_rx = crate::server::peer_support::register_peer_channel(
&app_state,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol.clone(),
app_state.clone(),
server_id,
connection_id.as_u32(),
write_half.clone(),
status_tx.clone(),
);
let result = Self::run_session(
read_half,
&write_half,
peer_addr,
connection_id,
llm_client,
&app_state,
&status_tx,
server_id,
&protocol,
)
.await;
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
let _ = write_half.lock().await.shutdown().await;
app_state
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
result
}
async fn write_counted(
write_half: &SharedWrite,
data: &[u8],
app_state: &AppState,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
) -> std::io::Result<()> {
use tokio::io::AsyncWriteExt;
{
let mut writer = write_half.lock().await;
writer.write_all(data).await?;
writer.flush().await?;
}
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(data.len() as u64),
None,
Some(1),
)
.await;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn run_session(
mut read_half: tokio::io::ReadHalf<tokio::net::TcpStream>,
write_half: &SharedWrite,
peer_addr: SocketAddr,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
protocol: &Arc<TorrentTrackerProtocol>,
) -> Result<()> {
use tokio::io::AsyncReadExt;
let mut buffer = vec![0u8; 8192];
let n = match tokio::time::timeout(REQUEST_READ_TIMEOUT, read_half.read(&mut buffer)).await
{
Ok(result) => result?,
Err(_) => {
Log::new(Some(status_tx)).warn(format!(
"BitTorrent Tracker {} sent no request within {}s, closing \
decision=fail_closed_read_timeout",
peer_addr,
REQUEST_READ_TIMEOUT.as_secs()
));
let body = b"HTTP/1.1 408 Request Timeout\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
let _ = Self::write_counted(write_half, body, app_state, server_id, connection_id)
.await;
return Ok(());
}
};
if n == 0 {
Log::new(Some(status_tx)).debug("BitTorrent Tracker connection closed by peer");
return Ok(());
}
let request_data = buffer[..n].to_vec();
app_state
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
Log::new(Some(status_tx)).debug(format!(
"BitTorrent Tracker received {} bytes from {}",
n, peer_addr
));
if let Ok(request_str) = std::str::from_utf8(&request_data) {
Log::new(Some(status_tx)).trace(format!("BitTorrent Tracker request: {}", request_str));
}
let request_str = String::from_utf8_lossy(&request_data);
let (request_type, request_params) = match Self::parse_http_request(&request_str) {
Ok(parsed) => parsed,
Err(e) => {
Log::new(Some(status_tx)).warn(format!("BitTorrent Tracker bad request: {}", e));
let body =
b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
Self::write_counted(write_half, body, app_state, server_id, connection_id).await?;
return Ok(());
}
};
Log::new(Some(status_tx))
.debug(format!("BitTorrent Tracker request type: {}", request_type));
let event_type = match request_type.as_str() {
"announce" => &actions::TRACKER_ANNOUNCE_REQUEST_EVENT,
"scrape" => &actions::TRACKER_SCRAPE_REQUEST_EVENT,
other => {
tracing::warn!(
"BitTorrent Tracker: unrecognised path type '{}', routing to \
tracker_announce_request",
other
);
&actions::TRACKER_ANNOUNCE_REQUEST_EVENT
}
};
let event = Event::new(event_type, serde_json::json!(request_params));
Log::new(Some(status_tx)).debug(format!(
"BitTorrent Tracker calling LLM for {} request",
request_type
));
match call_llm(
&llm_client,
app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
for message in &execution_result.messages {
Log::new(Some(status_tx)).info(format!("{}", message));
}
Log::new(Some(status_tx)).debug(format!(
"BitTorrent Tracker got {} protocol results",
execution_result.protocol_results.len()
));
let model_rejected = execution_result
.raw_actions
.iter()
.any(|a| a.get("type").and_then(|t| t.as_str()) == Some("send_error_response"));
let mut sent_any = false;
for protocol_result in execution_result.protocol_results {
if let Some(output_data) = protocol_result.get_all_output().first() {
sent_any = true;
Self::write_counted(
write_half,
output_data,
app_state,
server_id,
connection_id,
)
.await?;
Log::new(Some(status_tx)).debug(format!(
"BitTorrent Tracker sent {} bytes to {}",
output_data.len(),
peer_addr
));
if let Ok(response_str) = std::str::from_utf8(output_data) {
Log::new(Some(status_tx))
.trace(format!("BitTorrent Tracker response: {}", response_str));
}
}
}
if sent_any {
Log::new(Some(status_tx)).debug(format!(
"BitTorrent Tracker {} from {} decision={}",
request_type,
peer_addr,
if model_rejected {
"model_reject"
} else {
"model_answer"
}
));
} else {
Log::new(Some(status_tx)).warn(format!(
"BitTorrent Tracker {} from {} decision=fail_closed_no_action",
request_type, peer_addr
));
Self::write_failure_response(
write_half,
WireFailure::Unavailable,
connection_id,
server_id,
app_state,
)
.await?;
}
}
Err(e) => {
let failure = WireFailure::classify(&e);
Log::new(Some(status_tx)).error(format!(
"BitTorrent Tracker {} from {} decision=fail_closed_llm_error \
category={:?}: {}",
request_type, peer_addr, failure, e
));
Self::write_failure_response(
write_half,
failure,
connection_id,
server_id,
app_state,
)
.await?;
}
}
Ok(())
}
async fn write_failure_response(
write_half: &SharedWrite,
failure: WireFailure,
connection_id: ConnectionId,
server_id: crate::state::ServerId,
app_state: &Arc<AppState>,
) -> Result<()> {
let mut body_dict = std::collections::HashMap::new();
body_dict.insert(
b"failure reason".to_vec(),
serde_bencode::value::Value::Bytes(failure.text().as_bytes().to_vec()),
);
let body = serde_bencode::to_bytes(&serde_bencode::value::Value::Dict(body_dict))
.unwrap_or_default();
let head = if failure.is_overloaded() {
format!(
"HTTP/1.1 503 Service Unavailable\r\nRetry-After: 30\r\nContent-Type: \
text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
)
} else {
format!(
"HTTP/1.1 500 Internal Server Error\r\nContent-Type: \
text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
)
};
let mut response = head.into_bytes();
response.extend_from_slice(&body);
Self::write_counted(write_half, &response, app_state, server_id, connection_id).await?;
Ok(())
}
fn parse_http_request(request: &str) -> Result<(String, HashMap<String, serde_json::Value>)> {
let first_line = request
.lines()
.next()
.ok_or_else(|| anyhow::anyhow!("Empty request"))?;
let parts: Vec<&str> = first_line.split_whitespace().collect();
if parts.len() < 2 {
return Err(anyhow::anyhow!("Invalid HTTP request"));
}
let path = parts[1];
let request_type = if path.starts_with("/announce") {
"announce"
} else if path.starts_with("/scrape") {
"scrape"
} else {
"unknown"
};
let mut params = HashMap::new();
params.insert(
"request_type".to_string(),
serde_json::json!(request_type.to_string()),
);
params.insert("path".to_string(), serde_json::json!(path.to_string()));
params.insert("compact".to_string(), serde_json::json!(0u64));
if let Some(query_start) = path.find('?') {
let query = &path[query_start + 1..];
for param in query.split('&') {
if let Some(eq_pos) = param.find('=') {
let key = urlencoding::decode(¶m[..eq_pos])
.unwrap_or_else(|_| std::borrow::Cow::Borrowed(¶m[..eq_pos]))
.into_owned();
if key == "info_hash" || key == "peer_id" {
let value_str = ¶m[eq_pos + 1..];
let bytes = percent_decode_bytes(value_str);
params.insert(key, serde_json::json!(hex::encode(&bytes)));
} else {
let value = urlencoding::decode(¶m[eq_pos + 1..])
.unwrap_or_else(|_| std::borrow::Cow::Borrowed(¶m[eq_pos + 1..]))
.into_owned();
if key == "port"
|| key == "uploaded"
|| key == "downloaded"
|| key == "left"
|| key == "numwant"
|| key == "compact"
{
if let Ok(num) = value.parse::<u64>() {
params.insert(key, serde_json::json!(num));
} else {
params.insert(key, serde_json::json!(value));
}
} else {
params.insert(key, serde_json::json!(value));
}
}
}
}
}
Ok((request_type.to_string(), params))
}
}
fn percent_decode_bytes(s: &str) -> Vec<u8> {
let mut result = Vec::new();
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '%' {
let hex: String = chars.by_ref().take(2).collect();
if hex.len() == 2 {
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
result.push(byte);
continue;
}
}
result.push(b'%');
result.extend(hex.bytes());
} else {
result.extend(ch.to_string().bytes());
}
}
result
}