1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! Extension plugin interface for custom BEP 10 extensions.
//!
//! Plugins implement [`ExtensionPlugin`] to handle custom extension messages
//! without modifying torrent internals. Built-in extensions (`ut_metadata`,
//! `ut_pex`, `lt_trackers`) are hard-coded; plugins receive IDs starting at 10.
use std::collections::BTreeMap;
use std::net::SocketAddr;
use irontide_bencode::BencodeValue;
use irontide_core::Id20;
use irontide_wire::ExtHandshake;
/// A custom BEP 10 extension message handler.
///
/// Implement this trait to add custom extension protocol support to a torrent
/// session. Plugins are registered via `ClientBuilder::add_extension()` and
/// are immutable after session start.
///
/// # Extension ID allocation
///
/// Built-in extensions occupy IDs 1-3:
/// - `ut_metadata` = 1
/// - `ut_pex` = 2
/// - `lt_trackers` = 3
///
/// Plugins are assigned IDs starting at 10, in registration order.
///
/// # Constraints
///
/// - Plugins cannot access torrent internals (piece state, peer list).
/// - Plugins cannot initiate unsolicited messages -- respond only.
/// - Plugins cannot override built-in extensions.
/// - Callbacks are synchronous -- spawn tasks internally for async work.
pub trait ExtensionPlugin: Send + Sync + 'static {
/// Extension name for BEP 10 handshake negotiation (e.g. `"ut_comment"`).
///
/// This name is advertised in the extension handshake `m` dictionary.
fn name(&self) -> &str;
/// Called when a peer's extension handshake arrives.
///
/// Return extra key-value pairs to merge into our handshake response,
/// or `None` to add nothing.
fn on_handshake(
&self,
_info_hash: &Id20,
_peer_addr: SocketAddr,
_handshake: &ExtHandshake,
) -> Option<BTreeMap<String, BencodeValue>> {
None
}
/// Called when an extension message for this plugin arrives.
///
/// Return an optional response payload to send back to the peer.
fn on_message(
&self,
_info_hash: &Id20,
_peer_addr: SocketAddr,
_payload: &[u8],
) -> Option<Vec<u8>> {
None
}
/// Peer connected (after BT handshake, before extension handshake).
fn on_peer_connected(&self, _info_hash: &Id20, _peer_addr: SocketAddr) {}
/// Peer disconnected.
fn on_peer_disconnected(&self, _info_hash: &Id20, _peer_addr: SocketAddr) {}
}
#[cfg(test)]
mod tests {
use super::*;
/// A minimal test plugin that echoes messages back.
struct EchoPlugin;
impl ExtensionPlugin for EchoPlugin {
fn name(&self) -> &'static str {
"ut_echo"
}
fn on_message(
&self,
_info_hash: &Id20,
_peer_addr: SocketAddr,
payload: &[u8],
) -> Option<Vec<u8>> {
Some(payload.to_vec())
}
}
#[test]
fn plugin_as_trait_object() {
let plugin: Box<dyn ExtensionPlugin> = Box::new(EchoPlugin);
assert_eq!(plugin.name(), "ut_echo");
}
#[test]
fn plugin_echo_response() {
let plugin = EchoPlugin;
let info_hash = Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
let addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
let response = plugin.on_message(&info_hash, addr, b"hello");
assert_eq!(response, Some(b"hello".to_vec()));
}
#[test]
fn default_lifecycle_hooks_are_noops() {
let plugin = EchoPlugin;
let info_hash = Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
let addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
// These should not panic
plugin.on_peer_connected(&info_hash, addr);
plugin.on_peer_disconnected(&info_hash, addr);
// Default handshake returns None
let hs = ExtHandshake::new();
assert!(plugin.on_handshake(&info_hash, addr, &hs).is_none());
}
#[test]
fn plugin_vec_in_arc() {
use std::sync::Arc;
let plugins: Arc<Vec<Box<dyn ExtensionPlugin>>> = Arc::new(vec![Box::new(EchoPlugin)]);
assert_eq!(plugins.len(), 1);
assert_eq!(plugins[0].name(), "ut_echo");
}
}