1pub mod actions;
3
4pub use actions::TorrentPeerClientProtocol;
5
6use anyhow::{bail, Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::TcpStream;
11use tokio::sync::{mpsc, Mutex};
12use tracing::{error, info, trace};
13
14use crate::client::llm_budget::call_llm_for_client;
15use crate::client::torrent_peer::actions::{PEER_HANDSHAKE_EVENT, PEER_MESSAGE_EVENT};
16use crate::llm::ollama_client::OllamaClient;
17use crate::llm::ClientLlmResult;
18use crate::protocol::Event;
19use crate::state::app_state::AppState;
20use crate::state::{ClientId, ClientStatus};
21
22pub const MAX_PEER_MESSAGE_BYTES: usize = 8 * 1024 * 1024;
25
26pub async fn read_peer_message<R: AsyncReadExt + Unpin>(reader: &mut R) -> Result<Vec<u8>> {
29 read_peer_message_with_timeout(reader, crate::client::response_reader::RESPONSE_DEADLINE).await
30}
31
32pub async fn read_peer_message_with_timeout<R: AsyncReadExt + Unpin>(
33 reader: &mut R,
34 deadline: std::time::Duration,
35) -> Result<Vec<u8>> {
36 let first = reader.read_u8().await?;
37 tokio::time::timeout(deadline, async {
38 let mut length = [first, 0, 0, 0];
39 reader.read_exact(&mut length[1..]).await?;
40 let len = u32::from_be_bytes(length) as usize;
41 if len > MAX_PEER_MESSAGE_BYTES {
42 bail!("peer message length {len} exceeds the {MAX_PEER_MESSAGE_BYTES}-byte cap");
43 }
44 let mut message = vec![0; len];
45 reader.read_exact(&mut message).await?;
46 Ok(message)
47 })
48 .await
49 .map_err(|_| anyhow::anyhow!("peer message deadline exceeded"))?
50}
51
52#[repr(u8)]
54#[allow(dead_code)]
55enum MessageType {
56 Choke = 0,
57 Unchoke = 1,
58 Interested = 2,
59 NotInterested = 3,
60 Have = 4,
61 Bitfield = 5,
62 Request = 6,
63 Piece = 7,
64 Cancel = 8,
65 Port = 9,
66}
67
68pub struct TorrentPeerClient;
70
71impl TorrentPeerClient {
72 pub async fn connect_with_llm_actions(
74 remote_addr: String,
75 llm_client: OllamaClient,
76 app_state: Arc<AppState>,
77 status_tx: mpsc::UnboundedSender<String>,
78 client_id: ClientId,
79 ) -> Result<SocketAddr> {
80 let stream = TcpStream::connect(&remote_addr)
82 .await
83 .context(format!("Failed to connect to peer at {}", remote_addr))?;
84
85 let local_addr = stream.local_addr()?;
86 let remote_sock_addr = stream.peer_addr()?;
87
88 info!(
89 "BitTorrent Peer client {} connected to {} (local: {})",
90 client_id, remote_sock_addr, local_addr
91 );
92
93 app_state
95 .update_client_status(client_id, ClientStatus::Connected)
96 .await;
97 let _ = status_tx.send(format!(
98 "[CLIENT] BitTorrent Peer client {} connected",
99 client_id
100 ));
101 let _ = status_tx.send("__UPDATE_UI__".to_string());
102
103 let (mut read_half, write_half) = tokio::io::split(stream);
105 let write_half_arc = Arc::new(Mutex::new(write_half));
106
107 let command_rx =
113 crate::client::command_support::register_command_channel(&app_state, client_id).await;
114 let cmd_state = app_state.clone();
115 let cmd_tx = status_tx.clone();
116 let cmd_write = write_half_arc.clone();
117 let cmd_protocol = Arc::new(TorrentPeerClientProtocol::new());
118 let cmd_task = tokio::spawn(async move {
119 Self::command_loop(
120 command_rx,
121 cmd_protocol,
122 cmd_write,
123 client_id,
124 cmd_state,
125 cmd_tx,
126 )
127 .await;
128 });
129 app_state.register_client_task(client_id, cmd_task).await;
130
131 let app_state_clone = app_state.clone();
133 let status_tx_clone = status_tx.clone();
134 let write_half_clone = write_half_arc.clone();
135 let llm_client_clone = llm_client.clone();
136 let task_registrar = app_state.clone();
139 let task_handle = tokio::spawn(async move {
140 let mut handshake_buf = vec![0u8; 68]; let handshake_result = tokio::time::timeout(
143 crate::client::response_reader::RESPONSE_DEADLINE,
144 read_half.read_exact(&mut handshake_buf),
145 )
146 .await
147 .unwrap_or_else(|_| {
148 Err(std::io::Error::new(
149 std::io::ErrorKind::TimedOut,
150 "peer handshake deadline exceeded",
151 ))
152 });
153 match handshake_result {
154 Ok(_) => {
155 trace!("Peer client {} received handshake", client_id);
156
157 let pstrlen = handshake_buf[0];
159 if pstrlen == 19 && &handshake_buf[1..20] == b"BitTorrent protocol" {
160 let reserved = &handshake_buf[20..28];
161 let info_hash = &handshake_buf[28..48];
162 let peer_id = &handshake_buf[48..68];
163
164 trace!(
165 "Handshake: info_hash={:?}, peer_id={:?}",
166 hex::encode(info_hash),
167 hex::encode(peer_id)
168 );
169
170 if let Some(instruction) =
172 app_state_clone.get_instruction_for_client(client_id).await
173 {
174 let protocol = Arc::new(crate::client::torrent_peer::actions::TorrentPeerClientProtocol::new());
175 let event = Event::new(
176 &PEER_HANDSHAKE_EVENT,
177 serde_json::json!({
178 "info_hash": hex::encode(info_hash),
179 "peer_id": hex::encode(peer_id),
180 "reserved": hex::encode(reserved),
181 }),
182 );
183
184 let memory = app_state_clone
185 .get_memory_for_client(client_id)
186 .await
187 .unwrap_or_default();
188
189 match call_llm_for_client(
190 &llm_client_clone,
191 &app_state_clone,
192 client_id.to_string(),
193 &instruction,
194 &memory,
195 Some(&event),
196 protocol.as_ref(),
197 &status_tx_clone,
198 )
199 .await
200 {
201 Ok(ClientLlmResult {
202 actions,
203 memory_updates,
204 }) => {
205 if let Some(mem) = memory_updates {
206 app_state_clone.set_memory_for_client(client_id, mem).await;
207 }
208
209 for action in actions {
210 if let Err(e) = Self::execute_peer_action(
211 client_id,
212 action,
213 &write_half_clone,
214 protocol.as_ref(),
215 )
216 .await
217 {
218 error!("Failed to execute peer action: {}", e);
219 }
220 }
221 }
222 Err(e) => {
223 error!("LLM error for Peer client {}: {}", client_id, e);
224 }
225 }
226 }
227 }
228
229 loop {
232 match read_peer_message(&mut read_half).await {
233 Ok(msg_buf) => {
234 if msg_buf.is_empty() {
235 trace!("Peer client {} received keep-alive", client_id);
236 continue;
237 }
238 let msg_type = msg_buf[0];
239 let payload = &msg_buf[1..];
240
241 trace!(
242 "Peer client {} received message type {}, len {}",
243 client_id,
244 msg_type,
245 payload.len()
246 );
247
248 if let Some(instruction) =
250 app_state_clone.get_instruction_for_client(client_id).await
251 {
252 let protocol = Arc::new(crate::client::torrent_peer::actions::TorrentPeerClientProtocol::new());
253 let event = Event::new(
254 &PEER_MESSAGE_EVENT,
255 serde_json::json!({
256 "message_type": msg_type,
257 "payload_len": payload.len(),
258 "payload_hex": hex::encode(payload),
259 }),
260 );
261
262 let memory = app_state_clone
263 .get_memory_for_client(client_id)
264 .await
265 .unwrap_or_default();
266
267 match call_llm_for_client(
268 &llm_client_clone,
269 &app_state_clone,
270 client_id.to_string(),
271 &instruction,
272 &memory,
273 Some(&event),
274 protocol.as_ref(),
275 &status_tx_clone,
276 )
277 .await
278 {
279 Ok(ClientLlmResult {
280 actions,
281 memory_updates,
282 }) => {
283 if let Some(mem) = memory_updates {
284 app_state_clone
285 .set_memory_for_client(client_id, mem)
286 .await;
287 }
288
289 for action in actions {
290 if let Err(e) = Self::execute_peer_action(
291 client_id,
292 action,
293 &write_half_clone,
294 protocol.as_ref(),
295 )
296 .await
297 {
298 error!("Failed to execute peer action: {}", e);
299 }
300 }
301 }
302 Err(e) => {
303 error!("LLM error: {}", e);
304 }
305 }
306 }
307 }
308 Err(e) => {
309 error!("Peer client {} message read error: {}", client_id, e);
310 break;
311 }
312 }
313 }
314 }
315 Err(e) => {
316 error!("Peer client {} handshake read error: {}", client_id, e);
317 }
318 }
319
320 app_state_clone.remove_client_handle(client_id).await;
324 app_state_clone
325 .update_client_status(client_id, ClientStatus::Disconnected)
326 .await;
327 let _ = status_tx_clone.send(format!(
328 "[CLIENT] BitTorrent Peer client {} disconnected",
329 client_id
330 ));
331 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
332 });
333 task_registrar
334 .register_client_task(client_id, task_handle)
335 .await;
336
337 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
339 let protocol =
340 Arc::new(crate::client::torrent_peer::actions::TorrentPeerClientProtocol::new());
341 let event = Event::new(
342 &PEER_HANDSHAKE_EVENT,
343 serde_json::json!({
344 "status": "connected",
345 "remote_addr": remote_sock_addr.to_string(),
346 }),
347 );
348
349 let memory = app_state
350 .get_memory_for_client(client_id)
351 .await
352 .unwrap_or_default();
353 let write_half_clone = write_half_arc.clone();
354
355 let task_registrar = app_state.clone();
358 let task_handle = tokio::spawn(async move {
359 match call_llm_for_client(
360 &llm_client,
361 &app_state,
362 client_id.to_string(),
363 &instruction,
364 &memory,
365 Some(&event),
366 protocol.as_ref(),
367 &status_tx,
368 )
369 .await
370 {
371 Ok(ClientLlmResult {
372 actions,
373 memory_updates,
374 }) => {
375 if let Some(mem) = memory_updates {
376 app_state.set_memory_for_client(client_id, mem).await;
377 }
378
379 for action in actions {
380 if let Err(e) = Self::execute_peer_action(
381 client_id,
382 action,
383 &write_half_clone,
384 protocol.as_ref(),
385 )
386 .await
387 {
388 error!("Failed to execute peer action: {}", e);
389 }
390 }
391 }
392 Err(e) => {
393 error!("LLM error: {}", e);
394 }
395 }
396 });
397 task_registrar
398 .register_client_task(client_id, task_handle)
399 .await;
400 }
401
402 Ok(local_addr)
403 }
404
405 async fn execute_peer_action(
408 client_id: ClientId,
409 action: serde_json::Value,
410 write_half: &Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
411 protocol: &dyn crate::llm::actions::client_trait::Client,
412 ) -> Result<PeerApplied> {
413 use crate::llm::actions::client_trait::ClientActionResult;
414
415 match protocol.execute_action(action)? {
416 ClientActionResult::Custom { name, data } if name == "peer_handshake" => {
417 let info_hash_hex = data
418 .get("info_hash")
419 .and_then(|v| v.as_str())
420 .context("Missing info_hash")?;
421 let peer_id_hex = data
422 .get("peer_id")
423 .and_then(|v| v.as_str())
424 .context("Missing peer_id")?;
425
426 let info_hash = hex::decode(info_hash_hex)?;
427 let peer_id = hex::decode(peer_id_hex)?;
428
429 let mut handshake = Vec::new();
431 handshake.push(19); handshake.extend_from_slice(b"BitTorrent protocol"); handshake.extend_from_slice(&[0u8; 8]); handshake.extend_from_slice(&info_hash); handshake.extend_from_slice(&peer_id); write_half.lock().await.write_all(&handshake).await?;
438 trace!("Peer client {} sent handshake", client_id);
439 Ok(PeerApplied::Sent(handshake.len()))
440 }
441 ClientActionResult::Custom { name, data } if name == "peer_message" => {
442 let msg_type = data
443 .get("message_type")
444 .and_then(|v| v.as_u64())
445 .context("Missing message_type")?;
446 let msg_type = u8::try_from(msg_type).context("peer message_type exceeds u8")?;
447 let payload_hex = data.get("payload").and_then(|v| v.as_str()).unwrap_or("");
448 let payload = if !payload_hex.is_empty() {
449 hex::decode(payload_hex)?
450 } else {
451 vec![]
452 };
453
454 if payload.len() >= MAX_PEER_MESSAGE_BYTES {
456 anyhow::bail!("peer message exceeds byte cap");
457 }
458 let msg_len =
459 u32::try_from(1 + payload.len()).context("peer message length exceeds u32")?;
460 let mut message = Vec::new();
461 message.extend_from_slice(&msg_len.to_be_bytes());
462 message.push(msg_type);
463 message.extend_from_slice(&payload);
464
465 write_half.lock().await.write_all(&message).await?;
466 trace!("Peer client {} sent message type {}", client_id, msg_type);
467 Ok(PeerApplied::Sent(message.len()))
468 }
469 ClientActionResult::Disconnect => {
470 info!("Peer client {} disconnecting", client_id);
471 Ok(PeerApplied::Disconnect)
472 }
473 _ => Ok(PeerApplied::Nothing),
474 }
475 }
476
477 async fn command_loop(
485 mut command_rx: tokio::sync::mpsc::Receiver<crate::state::client_handles::ClientCommand>,
486 protocol: Arc<TorrentPeerClientProtocol>,
487 write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
488 client_id: ClientId,
489 app_state: Arc<AppState>,
490 status_tx: mpsc::UnboundedSender<String>,
491 ) {
492 use crate::llm::actions::protocol_trait::Protocol;
493 use crate::state::client_handles::ClientSendOutcome;
494 use crate::state::AccessLogOwner;
495
496 while let Some(command) = command_rx.recv().await {
497 let action = command.action.clone();
498 let outcome: anyhow::Result<ClientSendOutcome> = match Self::execute_peer_action(
499 client_id,
500 action.clone(),
501 &write_half,
502 protocol.as_ref(),
503 )
504 .await
505 {
506 Err(e) => Ok(ClientSendOutcome::Rejected {
507 error: e.to_string(),
508 }),
509 Ok(PeerApplied::Disconnect) => Ok(ClientSendOutcome::Disconnected),
510 Ok(PeerApplied::Sent(0)) | Ok(PeerApplied::Nothing) => {
511 Ok(ClientSendOutcome::Executed {
512 detail: "executed (nothing to write)".to_string(),
513 })
514 }
515 Ok(PeerApplied::Sent(bytes_sent)) => Ok(ClientSendOutcome::Sent { bytes_sent }),
516 };
517
518 let outcome_json = match &outcome {
519 Ok(o) => serde_json::to_value(o).unwrap_or(serde_json::Value::Null),
520 Err(e) => serde_json::json!({"error": e.to_string()}),
521 };
522 app_state
523 .record_access_log(
524 AccessLogOwner::Client(client_id.as_u32()),
525 protocol.protocol_name(),
526 None,
527 "injected_action",
528 action,
529 vec![outcome_json],
530 )
531 .await;
532
533 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
534 if let Err(e) = &outcome {
535 error!("Peer client {} injected action failed: {}", client_id, e);
536 let _ = status_tx.send(format!(
537 "[WARN] Client {} injected action failed: {}",
538 client_id, e
539 ));
540 }
541 let _ = status_tx.send("__UPDATE_UI__".to_string());
542 crate::client::command_support::reply(command, outcome);
543
544 if disconnect {
545 let _ = write_half.lock().await.shutdown().await;
548 break;
549 }
550 }
551 }
552}
553
554enum PeerApplied {
556 Sent(usize),
558 Disconnect,
560 Nothing,
562}