pub mod actions;
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::mpsc;
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::TorrentPeerProtocol;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Disposition {
Continue,
Close,
}
const CHOKE_FRAME: [u8; 5] = [0x00, 0x00, 0x00, 0x01, 0x00];
const HANDSHAKE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
#[derive(Clone, Copy)]
struct ReadDeadlines {
first_byte: std::time::Duration,
idle: std::time::Duration,
}
impl ReadDeadlines {
fn resolve(first_byte_secs: Option<u64>, idle_secs: Option<u64>) -> Self {
Self {
first_byte: first_byte_secs
.map(std::time::Duration::from_secs)
.unwrap_or(HANDSHAKE_READ_TIMEOUT),
idle: idle_secs
.map(std::time::Duration::from_secs)
.unwrap_or(IDLE_AFTER_HANDSHAKE_TIMEOUT),
}
}
}
const IDLE_AFTER_HANDSHAKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(180);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"";
pub const MAX_PENDING: usize = 2 * 1024 * 1024;
pub struct TorrentPeerServer;
impl TorrentPeerServer {
#[allow(clippy::too_many_arguments)]
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,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> Result<SocketAddr> {
let deadlines = ReadDeadlines::resolve(first_byte_timeout_secs, idle_timeout_secs);
let listener = TcpListener::bind(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"BitTorrent Peer server (action-based) listening on {}",
local_addr
));
let protocol = Arc::new(TorrentPeerProtocol::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 Peer",
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 Peer accepted connection from {}",
peer_addr
));
let (read_half, write_half) = tokio::io::split(stream);
let write_half = Arc::new(tokio::sync::Mutex::new(write_half));
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(
read_half,
write_half,
peer_addr,
local_addr,
connection_id,
llm_clone,
state_clone,
status_clone,
server_id,
protocol_clone,
deadlines,
)
.await
{
error!("BitTorrent Peer connection error: {}", e);
}
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("BitTorrent Peer 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(
mut read_half: tokio::io::ReadHalf<tokio::net::TcpStream>,
write_half: Arc<tokio::sync::Mutex<tokio::io::WriteHalf<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<TorrentPeerProtocol>,
deadlines: ReadDeadlines,
) -> Result<()> {
use tokio::io::AsyncReadExt;
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: Result<()> = async {
let mut chunk = vec![0u8; 16384];
let mut pending: Vec<u8> = Vec::new();
let mut handshake_complete = false;
'read: loop {
let read_timeout = if handshake_complete {
deadlines.idle
} else {
deadlines.first_byte
};
let n = match tokio::time::timeout(read_timeout, read_half.read(&mut chunk)).await {
Ok(read) => read?,
Err(_) => {
Log::new(Some(&status_tx)).info(format!(
"BitTorrent Peer {} sent nothing for {}s; closing idle connection",
peer_addr,
read_timeout.as_secs()
));
break;
}
};
if n == 0 {
Log::new(Some(&status_tx)).debug("BitTorrent Peer connection closed by peer");
break;
}
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 Peer received {} bytes from {}",
n, peer_addr
));
Log::new(Some(&status_tx)).trace(format!(
"BitTorrent Peer data (hex): {}",
hex::encode(&chunk[..n])
));
pending.extend_from_slice(&chunk[..n]);
if pending.len() > MAX_PENDING {
Log::new(Some(&status_tx)).warn(format!(
"BitTorrent Peer {} buffered {} bytes without a complete message, closing",
peer_addr,
pending.len()
));
break;
}
loop {
if !handshake_complete {
if pending.len() < 68 {
break;
}
let handshake: Vec<u8> = pending.drain(..68).collect();
match Self::parse_handshake(&handshake) {
Ok((info_hash, peer_id, peer_id_hex)) => {
Log::new(Some(&status_tx)).debug(format!(
"BitTorrent Peer handshake: info_hash={}, peer_id={}",
info_hash, peer_id
));
handshake_complete = true;
let event = Event::new(
&actions::PEER_HANDSHAKE_EVENT,
serde_json::json!({
"info_hash": info_hash,
"peer_id": peer_id,
"peer_id_hex": peer_id_hex,
}),
);
let disposition = Self::dispatch_event(
event,
&write_half,
peer_addr,
connection_id,
&llm_client,
&app_state,
&status_tx,
server_id,
&protocol,
)
.await?;
if disposition == Disposition::Close {
break 'read;
}
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to parse handshake: {}", e));
break 'read;
}
}
continue;
}
if pending.len() < 4 {
break;
}
let length =
u32::from_be_bytes([pending[0], pending[1], pending[2], pending[3]])
as usize;
if pending.len() < 4 + length {
break;
}
let frame: Vec<u8> = pending.drain(..4 + length).collect();
match Self::parse_message(&frame) {
Ok((message_type, message_data)) => {
Log::new(Some(&status_tx))
.debug(format!("BitTorrent Peer message type: {}", message_type));
let event_type = match message_type.as_str() {
"choke" | "unchoke" | "interested" | "not_interested" => {
&actions::PEER_CHOKE_MESSAGE_EVENT
}
"request" => &actions::PEER_REQUEST_MESSAGE_EVENT,
"bitfield" => &actions::PEER_BITFIELD_MESSAGE_EVENT,
_ => &actions::PEER_MESSAGE_EVENT,
};
let event = Event::new(event_type, message_data);
Log::new(Some(&status_tx)).debug(format!(
"BitTorrent Peer calling LLM for {} message",
message_type
));
let disposition = Self::dispatch_event(
event,
&write_half,
peer_addr,
connection_id,
&llm_client,
&app_state,
&status_tx,
server_id,
&protocol,
)
.await?;
if disposition == Disposition::Close {
break 'read;
}
}
Err(e) => {
Log::new(Some(&status_tx))
.warn(format!("Failed to parse peer message: {}", e));
}
}
}
}
Ok(())
}
.await;
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
result
}
#[allow(clippy::too_many_arguments)]
async fn dispatch_event(
event: Event,
write_half: &Arc<tokio::sync::Mutex<tokio::io::WriteHalf<tokio::net::TcpStream>>>,
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<TorrentPeerProtocol>,
) -> Result<Disposition> {
match call_llm(
llm_client,
app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
Self::process_llm_response(
execution_result,
write_half,
peer_addr,
connection_id,
app_state,
server_id,
status_tx,
)
.await?;
Ok(Disposition::Continue)
}
Err(e) => {
use tokio::io::AsyncWriteExt;
let failure = WireFailure::classify(&e);
let log = Log::new(Some(&status_tx));
log.error(format!(
"BitTorrent Peer {} conn={} decision=fail_closed_llm_error category={} LLM error: {}",
peer_addr,
connection_id.as_u32(),
if failure.is_overloaded() { "overloaded" } else { "unavailable" },
e
));
let write_result = {
let mut write = write_half.lock().await;
let sent = write.write_all(&CHOKE_FRAME).await;
if sent.is_ok() && !failure.is_overloaded() {
let _ = write.shutdown().await;
}
sent
};
match write_result {
Ok(()) => {
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(CHOKE_FRAME.len() as u64),
None,
Some(1),
)
.await;
}
Err(write_err) => {
log.warn(format!(
"BitTorrent Peer could not send choke to {}: {}",
peer_addr, write_err
));
return Ok(Disposition::Close);
}
}
if failure.is_overloaded() {
log.warn(format!(
"BitTorrent Peer choked {} (transient); connection kept open",
peer_addr
));
Ok(Disposition::Continue)
} else {
log.info(format!(
"BitTorrent Peer choked and half-closed {} after LLM error",
peer_addr
));
Ok(Disposition::Close)
}
}
}
}
#[allow(clippy::too_many_arguments)]
async fn process_llm_response(
execution_result: crate::llm::actions::executor::ExecutionResult,
write_half: &Arc<tokio::sync::Mutex<tokio::io::WriteHalf<tokio::net::TcpStream>>>,
peer_addr: SocketAddr,
connection_id: ConnectionId,
app_state: &Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
use tokio::io::AsyncWriteExt;
for message in &execution_result.messages {
Log::new(Some(&status_tx)).info(format!("{}", message));
}
Log::new(Some(&status_tx)).debug(format!(
"BitTorrent Peer got {} protocol results",
execution_result.protocol_results.len()
));
let model_refused = execution_result
.raw_actions
.iter()
.any(|a| a.get("type").and_then(|t| t.as_str()) == Some("send_choke"));
let decision = if model_refused {
"model_reject"
} else if execution_result.protocol_results.is_empty() {
"model_no_action"
} else {
"model_answer"
};
Log::new(Some(&status_tx)).debug(format!(
"BitTorrent Peer {} conn={} decision={}",
peer_addr,
connection_id.as_u32(),
decision
));
for protocol_result in execution_result.protocol_results {
for output_data in protocol_result.get_all_output() {
let mut write = write_half.lock().await;
write.write_all(&output_data).await?;
drop(write);
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(output_data.len() as u64),
None,
Some(1),
)
.await;
Log::new(Some(&status_tx)).debug(format!(
"BitTorrent Peer sent {} bytes to {}",
output_data.len(),
peer_addr
));
let hex_str = hex::encode(&output_data);
Log::new(Some(&status_tx))
.trace(format!("BitTorrent Peer sent (hex): {}", hex_str));
}
}
Ok(())
}
fn parse_handshake(data: &[u8]) -> Result<(String, String, String)> {
if data.len() < 68 {
return Err(anyhow::anyhow!("Handshake too short"));
}
let pstrlen = data[0] as usize;
if pstrlen != 19 {
return Err(anyhow::anyhow!("Invalid pstrlen"));
}
let pstr = &data[1..20];
if pstr != b"BitTorrent protocol" {
return Err(anyhow::anyhow!("Invalid protocol string"));
}
let info_hash = hex::encode(&data[28..48]);
let peer_id = String::from_utf8_lossy(&data[48..68]).to_string();
let peer_id_hex = hex::encode(&data[48..68]);
Ok((info_hash, peer_id, peer_id_hex))
}
fn parse_message(data: &[u8]) -> Result<(String, serde_json::Value)> {
if data.is_empty() {
return Ok((
"keepalive".to_string(),
serde_json::json!({"message_type": "keepalive"}),
));
}
if data.len() < 4 {
return Err(anyhow::anyhow!("Message too short"));
}
let length = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
if length == 0 {
return Ok((
"keepalive".to_string(),
serde_json::json!({"message_type": "keepalive"}),
));
}
if data.len() < 4 + length {
return Err(anyhow::anyhow!("Incomplete message"));
}
let message_id = data[4];
let payload = &data[5..4 + length];
let (message_type, message_data) = match message_id {
0 => ("choke", serde_json::json!({})),
1 => ("unchoke", serde_json::json!({})),
2 => ("interested", serde_json::json!({})),
3 => ("not_interested", serde_json::json!({})),
4 => {
if payload.len() >= 4 {
let piece_index =
u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
("have", serde_json::json!({"piece_index": piece_index}))
} else {
("have", serde_json::json!({}))
}
}
5 => {
(
"bitfield",
serde_json::json!({"bitfield": hex::encode(payload)}),
)
}
6 => {
if payload.len() >= 12 {
let index =
u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
let begin =
u32::from_be_bytes([payload[4], payload[5], payload[6], payload[7]]);
let length =
u32::from_be_bytes([payload[8], payload[9], payload[10], payload[11]]);
(
"request",
serde_json::json!({"index": index, "begin": begin, "length": length}),
)
} else {
("request", serde_json::json!({}))
}
}
7 => {
if payload.len() >= 8 {
let index =
u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
let begin =
u32::from_be_bytes([payload[4], payload[5], payload[6], payload[7]]);
let block = hex::encode(&payload[8..]);
(
"piece",
serde_json::json!({"index": index, "begin": begin, "block_hex": block}),
)
} else {
("piece", serde_json::json!({}))
}
}
8 => {
if payload.len() >= 12 {
let index =
u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
let begin =
u32::from_be_bytes([payload[4], payload[5], payload[6], payload[7]]);
let length =
u32::from_be_bytes([payload[8], payload[9], payload[10], payload[11]]);
(
"cancel",
serde_json::json!({"index": index, "begin": begin, "length": length}),
)
} else {
("cancel", serde_json::json!({}))
}
}
_ => (
"unknown",
serde_json::json!({"id": message_id, "payload_hex": hex::encode(payload)}),
),
};
let mut message_data = message_data;
if let Some(obj) = message_data.as_object_mut() {
obj.insert("message_type".to_string(), serde_json::json!(message_type));
}
Ok((message_type.to_string(), message_data))
}
}