Merge branch 'develop': peer failure forwarding and thread-flood fixes
This commit is contained in:
@@ -5,7 +5,7 @@ use crate::message::peer::{
|
|||||||
FileSearchResponse, GetShareFileList, PeerInit, PlaceInQueueRequest,
|
FileSearchResponse, GetShareFileList, PeerInit, PlaceInQueueRequest,
|
||||||
PlaceInQueueResponse, QueueUploadHandler, SharedDirectory,
|
PlaceInQueueResponse, QueueUploadHandler, SharedDirectory,
|
||||||
SharedFileListResponseHandler, TransferRequest, TransferResponse,
|
SharedFileListResponseHandler, TransferRequest, TransferResponse,
|
||||||
UploadFailedHandler,
|
UploadDeniedHandler, UploadFailedHandler,
|
||||||
};
|
};
|
||||||
use crate::message::server::MessageFactory;
|
use crate::message::server::MessageFactory;
|
||||||
use crate::message::{Handlers, Message, MessageReader, MessageType};
|
use crate::message::{Handlers, Message, MessageReader, MessageType};
|
||||||
@@ -178,6 +178,7 @@ impl PeerActor {
|
|||||||
handlers.register_handler(TransferRequest);
|
handlers.register_handler(TransferRequest);
|
||||||
handlers.register_handler(TransferResponse);
|
handlers.register_handler(TransferResponse);
|
||||||
handlers.register_handler(GetShareFileList);
|
handlers.register_handler(GetShareFileList);
|
||||||
|
handlers.register_handler(UploadDeniedHandler);
|
||||||
handlers.register_handler(UploadFailedHandler);
|
handlers.register_handler(UploadFailedHandler);
|
||||||
handlers.register_handler(PlaceInQueueRequest);
|
handlers.register_handler(PlaceInQueueRequest);
|
||||||
handlers.register_handler(PlaceInQueueResponse);
|
handlers.register_handler(PlaceInQueueResponse);
|
||||||
@@ -282,8 +283,8 @@ impl PeerActor {
|
|||||||
PeerMessage::ProcessRead => {
|
PeerMessage::ProcessRead => {
|
||||||
self.process_read();
|
self.process_read();
|
||||||
}
|
}
|
||||||
PeerMessage::UploadFailed(username, filename) => {
|
PeerMessage::UploadFailed(_, filename) => {
|
||||||
self.handle_upload_failed(username, filename);
|
self.handle_upload_failed(filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,7 +453,8 @@ impl PeerActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_upload_failed(&self, username: String, filename: String) {
|
fn handle_upload_failed(&self, filename: String) {
|
||||||
|
let username = self.peer_username();
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.client_channel
|
.client_channel
|
||||||
.send(ClientOperation::UploadFailed(username, filename))
|
.send(ClientOperation::UploadFailed(username, filename))
|
||||||
@@ -658,6 +660,7 @@ impl PeerActor {
|
|||||||
debug!("[peer:{}] disconnect: {}", username, error);
|
debug!("[peer:{}] disconnect: {}", username, error);
|
||||||
|
|
||||||
self.stream.take();
|
self.stream.take();
|
||||||
|
self.connection_state = ConnectionState::Disconnected;
|
||||||
|
|
||||||
if self.disconnect_reported {
|
if self.disconnect_reported {
|
||||||
return;
|
return;
|
||||||
@@ -687,6 +690,7 @@ impl PeerActor {
|
|||||||
debug!("[peer:{}] disconnect", username);
|
debug!("[peer:{}] disconnect", username);
|
||||||
|
|
||||||
self.stream.take();
|
self.stream.take();
|
||||||
|
self.connection_state = ConnectionState::Disconnected;
|
||||||
|
|
||||||
if self.disconnect_reported {
|
if self.disconnect_reported {
|
||||||
return;
|
return;
|
||||||
@@ -927,21 +931,66 @@ mod tests {
|
|||||||
stream.set_nonblocking(true).unwrap();
|
stream.set_nonblocking(true).unwrap();
|
||||||
let (far_end, _) = listener.accept().unwrap();
|
let (far_end, _) = listener.accept().unwrap();
|
||||||
|
|
||||||
|
let (mut actor, rx) = make_actor(Some(stream));
|
||||||
|
actor.on_start();
|
||||||
|
(actor, rx, far_end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_actor(
|
||||||
|
stream: Option<TcpStream>,
|
||||||
|
) -> (PeerActor, Receiver<ClientOperation>) {
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
let peer = Peer::new(
|
let peer = Peer::new(
|
||||||
"bob".to_string(),
|
"bob".to_string(),
|
||||||
ConnectionType::P,
|
ConnectionType::P,
|
||||||
"127.0.0.1".to_string(),
|
"127.0.0.1".to_string(),
|
||||||
u32::from(addr.port()),
|
0,
|
||||||
None,
|
None,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
let mut actor =
|
let actor = PeerActor::new(peer, stream, None, tx, "me".to_string(), 7);
|
||||||
PeerActor::new(peer, Some(stream), None, tx, "me".to_string(), 7);
|
(actor, rx)
|
||||||
actor.on_start();
|
}
|
||||||
(actor, rx, far_end)
|
|
||||||
|
#[test]
|
||||||
|
fn a_timed_out_connect_parks_the_actor_in_disconnected() {
|
||||||
|
let (mut actor, rx) = make_actor(None);
|
||||||
|
actor.connection_state = ConnectionState::Connecting {
|
||||||
|
since: Instant::now().checked_sub(Duration::from_secs(21)).unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
actor.tick();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(actor.connection_state, ConnectionState::Disconnected),
|
||||||
|
"a timed-out connect must leave Connecting"
|
||||||
|
);
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(ClientOperation::PeerConnectFailed(7, username)) => {
|
||||||
|
assert_eq!(username, "bob");
|
||||||
|
}
|
||||||
|
other => panic!("expected PeerConnectFailed, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_upload_failed_message_reaches_the_client_as_this_peer() {
|
||||||
|
let (mut actor, rx, _far_end) = connected_actor();
|
||||||
|
|
||||||
|
actor.handle_message(PeerMessage::UploadFailed(
|
||||||
|
String::new(),
|
||||||
|
"song.mp3".to_string(),
|
||||||
|
));
|
||||||
|
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(ClientOperation::UploadFailed(username, filename)) => {
|
||||||
|
assert_eq!(username, "bob");
|
||||||
|
assert_eq!(filename, "song.mp3");
|
||||||
|
}
|
||||||
|
other => panic!("expected UploadFailed, got {other:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ use crate::peer::Peer;
|
|||||||
use crate::utils::lock::MutexExt;
|
use crate::utils::lock::MutexExt;
|
||||||
use crate::{debug, error};
|
use crate::{debug, error};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
/// Source of unique per-actor ids so terminal-outcome eviction can be made
|
/// Source of unique per-actor ids so terminal-outcome eviction can be made
|
||||||
/// identity-aware (a replaced actor must not evict its replacement).
|
/// identity-aware (a replaced actor must not evict its replacement).
|
||||||
@@ -18,13 +19,14 @@ static NEXT_PEER_ID: AtomicU64 = AtomicU64::new(1);
|
|||||||
|
|
||||||
/// Registered peers keyed by username, each stored with the unique id of the
|
/// Registered peers keyed by username, each stored with the unique id of the
|
||||||
/// actor currently occupying the slot.
|
/// actor currently occupying the slot.
|
||||||
type PeerMap = HashMap<String, (u64, ActorHandle<PeerMessage>)>;
|
type PeerMap = HashMap<String, (u64, ActorHandle<PeerMessage>, Instant)>;
|
||||||
|
|
||||||
pub struct PeerRegistry {
|
pub struct PeerRegistry {
|
||||||
peers: Arc<Mutex<PeerMap>>,
|
peers: Arc<Mutex<PeerMap>>,
|
||||||
actor_system: Arc<ActorSystem>,
|
actor_system: Arc<ActorSystem>,
|
||||||
client_channel: Sender<ClientOperation>,
|
client_channel: Sender<ClientOperation>,
|
||||||
own_username: String,
|
own_username: String,
|
||||||
|
capacity: Arc<AtomicUsize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PeerRegistry {
|
impl PeerRegistry {
|
||||||
@@ -33,12 +35,28 @@ impl PeerRegistry {
|
|||||||
actor_system: Arc<ActorSystem>,
|
actor_system: Arc<ActorSystem>,
|
||||||
client_channel: Sender<ClientOperation>,
|
client_channel: Sender<ClientOperation>,
|
||||||
own_username: String,
|
own_username: String,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_capacity(
|
||||||
|
actor_system,
|
||||||
|
client_channel,
|
||||||
|
own_username,
|
||||||
|
Arc::new(AtomicUsize::new(usize::MAX)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn with_capacity(
|
||||||
|
actor_system: Arc<ActorSystem>,
|
||||||
|
client_channel: Sender<ClientOperation>,
|
||||||
|
own_username: String,
|
||||||
|
capacity: Arc<AtomicUsize>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
peers: Arc::new(Mutex::new(HashMap::new())),
|
peers: Arc::new(Mutex::new(HashMap::new())),
|
||||||
actor_system,
|
actor_system,
|
||||||
client_channel,
|
client_channel,
|
||||||
own_username,
|
own_username,
|
||||||
|
capacity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +65,16 @@ impl PeerRegistry {
|
|||||||
peer: Peer,
|
peer: Peer,
|
||||||
stream: Option<TcpStream>,
|
stream: Option<TcpStream>,
|
||||||
reader: Option<MessageReader>,
|
reader: Option<MessageReader>,
|
||||||
|
) -> Result<ActorHandle<PeerMessage>, String> {
|
||||||
|
self.register_peer_protected(peer, stream, reader, &HashSet::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn register_peer_protected(
|
||||||
|
&self,
|
||||||
|
peer: Peer,
|
||||||
|
stream: Option<TcpStream>,
|
||||||
|
reader: Option<MessageReader>,
|
||||||
|
protected: &HashSet<String>,
|
||||||
) -> Result<ActorHandle<PeerMessage>, String> {
|
) -> Result<ActorHandle<PeerMessage>, String> {
|
||||||
let username = peer.username.clone();
|
let username = peer.username.clone();
|
||||||
let id = NEXT_PEER_ID.fetch_add(1, Ordering::Relaxed);
|
let id = NEXT_PEER_ID.fetch_add(1, Ordering::Relaxed);
|
||||||
@@ -71,6 +99,28 @@ impl PeerRegistry {
|
|||||||
.lock_safe()
|
.lock_safe()
|
||||||
.map_err(|e| format!("peer registry lock poisoned: {e}"))?;
|
.map_err(|e| format!("peer registry lock poisoned: {e}"))?;
|
||||||
|
|
||||||
|
let capacity = self.capacity.load(Ordering::Relaxed).max(1);
|
||||||
|
if peers.len() >= capacity && !peers.contains_key(&username) {
|
||||||
|
let victim = peers
|
||||||
|
.iter()
|
||||||
|
.filter(|(name, _)| !protected.contains(name.as_str()))
|
||||||
|
.min_by_key(|(_, (_, _, registered))| *registered)
|
||||||
|
.map(|(name, _)| name.clone());
|
||||||
|
let Some(victim) = victim else {
|
||||||
|
return Err(format!(
|
||||||
|
"peer registry at capacity ({capacity}) and every peer \
|
||||||
|
is busy; refusing {username}"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if let Some((_, handle, _)) = peers.remove(&victim) {
|
||||||
|
let _ = handle.stop();
|
||||||
|
debug!(
|
||||||
|
"[peer_registry] evicted idle peer {} to admit {}",
|
||||||
|
victim, username
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let handle = self
|
let handle = self
|
||||||
.actor_system
|
.actor_system
|
||||||
.try_spawn_with_handle(actor, |actor, handle| {
|
.try_spawn_with_handle(actor, |actor, handle| {
|
||||||
@@ -81,8 +131,8 @@ impl PeerRegistry {
|
|||||||
// become an orphan pinning a pool worker forever. Eviction on the
|
// become an orphan pinning a pool worker forever. Eviction on the
|
||||||
// replaced actor's later shutdown is identity-aware (keyed on its id),
|
// replaced actor's later shutdown is identity-aware (keyed on its id),
|
||||||
// so stopping it here cannot evict this new connection.
|
// so stopping it here cannot evict this new connection.
|
||||||
if let Some((_, old_handle)) =
|
if let Some((_, old_handle, _)) =
|
||||||
peers.insert(username.clone(), (id, handle.clone()))
|
peers.insert(username.clone(), (id, handle.clone(), Instant::now()))
|
||||||
{
|
{
|
||||||
let _ = old_handle.stop();
|
let _ = old_handle.stop();
|
||||||
debug!(
|
debug!(
|
||||||
@@ -97,7 +147,9 @@ impl PeerRegistry {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_peer(&self, username: &str) -> Option<ActorHandle<PeerMessage>> {
|
pub fn get_peer(&self, username: &str) -> Option<ActorHandle<PeerMessage>> {
|
||||||
match self.peers.lock_safe() {
|
match self.peers.lock_safe() {
|
||||||
Ok(peers) => peers.get(username).map(|(_, handle)| handle.clone()),
|
Ok(peers) => {
|
||||||
|
peers.get(username).map(|(_, handle, _)| handle.clone())
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("[peer_registry] get_peer: {}", e);
|
error!("[peer_registry] get_peer: {}", e);
|
||||||
None
|
None
|
||||||
@@ -123,7 +175,7 @@ impl PeerRegistry {
|
|||||||
debug!("[peer_registry] Removed peer actor for {}", username);
|
debug!("[peer_registry] Removed peer actor for {}", username);
|
||||||
}
|
}
|
||||||
|
|
||||||
removed.map(|(_, handle)| handle)
|
removed.map(|(_, handle, _)| handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove and return the actor for `username` only if it is still the actor
|
/// Remove and return the actor for `username` only if it is still the actor
|
||||||
@@ -142,8 +194,11 @@ impl PeerRegistry {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if peers.get(username).is_some_and(|(stored, _)| *stored == id) {
|
if peers
|
||||||
let removed = peers.remove(username).map(|(_, handle)| handle);
|
.get(username)
|
||||||
|
.is_some_and(|(stored, _, _)| *stored == id)
|
||||||
|
{
|
||||||
|
let removed = peers.remove(username).map(|(_, handle, _)| handle);
|
||||||
debug!(
|
debug!(
|
||||||
"[peer_registry] Removed peer actor {} for {}",
|
"[peer_registry] Removed peer actor {} for {}",
|
||||||
id, username
|
id, username
|
||||||
@@ -192,6 +247,7 @@ impl Clone for PeerRegistry {
|
|||||||
actor_system: self.actor_system.clone(),
|
actor_system: self.actor_system.clone(),
|
||||||
client_channel: self.client_channel.clone(),
|
client_channel: self.client_channel.clone(),
|
||||||
own_username: self.own_username.clone(),
|
own_username: self.own_username.clone(),
|
||||||
|
capacity: self.capacity.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,8 +257,112 @@ mod tests {
|
|||||||
use super::PeerRegistry;
|
use super::PeerRegistry;
|
||||||
use crate::actor::ActorSystem;
|
use crate::actor::ActorSystem;
|
||||||
use crate::peer::{ConnectionType, Peer};
|
use crate::peer::{ConnectionType, Peer};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::net::{TcpListener, TcpStream};
|
use std::net::{TcpListener, TcpStream};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicUsize;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn loopback_peer(name: &str) -> (Peer, TcpStream, TcpStream) {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
let stream = TcpStream::connect(addr).unwrap();
|
||||||
|
stream.set_nonblocking(true).unwrap();
|
||||||
|
let far_end = listener.accept().unwrap().0;
|
||||||
|
let peer = Peer::new(
|
||||||
|
name.to_string(),
|
||||||
|
ConnectionType::P,
|
||||||
|
"127.0.0.1".to_string(),
|
||||||
|
u32::from(addr.port()),
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
(peer, stream, far_end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capped_registry(
|
||||||
|
capacity: usize,
|
||||||
|
) -> (
|
||||||
|
PeerRegistry,
|
||||||
|
std::sync::mpsc::Receiver<crate::client::ClientOperation>,
|
||||||
|
) {
|
||||||
|
let system = Arc::new(ActorSystem::new());
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
let registry = PeerRegistry::with_capacity(
|
||||||
|
system,
|
||||||
|
tx,
|
||||||
|
"me".to_string(),
|
||||||
|
Arc::new(AtomicUsize::new(capacity)),
|
||||||
|
);
|
||||||
|
(registry, rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capacity_evicts_the_oldest_idle_peer() {
|
||||||
|
let (registry, _rx) = capped_registry(2);
|
||||||
|
|
||||||
|
for name in ["a", "b", "c"] {
|
||||||
|
let (peer, stream, _far_end) = loopback_peer(name);
|
||||||
|
registry.register_peer(peer, Some(stream), None).unwrap();
|
||||||
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(!registry.contains("a"), "the oldest peer must be evicted");
|
||||||
|
assert!(registry.contains("b"));
|
||||||
|
assert!(registry.contains("c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capacity_refuses_when_every_peer_is_protected() {
|
||||||
|
let (registry, _rx) = capped_registry(1);
|
||||||
|
|
||||||
|
let (busy, busy_stream, _busy_far) = loopback_peer("busy");
|
||||||
|
registry
|
||||||
|
.register_peer(busy, Some(busy_stream), None)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let protected: HashSet<String> =
|
||||||
|
std::iter::once("busy".to_string()).collect();
|
||||||
|
let (newcomer, stream, _far) = loopback_peer("newcomer");
|
||||||
|
let result = registry.register_peer_protected(
|
||||||
|
newcomer,
|
||||||
|
Some(stream),
|
||||||
|
None,
|
||||||
|
&protected,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err(), "a full registry of busy peers must refuse");
|
||||||
|
assert!(registry.contains("busy"));
|
||||||
|
assert!(!registry.contains("newcomer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_returning_username_is_replaced_not_refused_at_capacity() {
|
||||||
|
let (registry, _rx) = capped_registry(1);
|
||||||
|
|
||||||
|
let (first, first_stream, _first_far) = loopback_peer("bob");
|
||||||
|
registry
|
||||||
|
.register_peer(first, Some(first_stream), None)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let protected: HashSet<String> =
|
||||||
|
std::iter::once("bob".to_string()).collect();
|
||||||
|
let (again, stream, _far) = loopback_peer("bob");
|
||||||
|
let result = registry.register_peer_protected(
|
||||||
|
again,
|
||||||
|
Some(stream),
|
||||||
|
None,
|
||||||
|
&protected,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"a reconnecting username must replace itself"
|
||||||
|
);
|
||||||
|
assert!(registry.contains("bob"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remove_peer_if_respects_actor_identity() {
|
fn remove_peer_if_respects_actor_identity() {
|
||||||
|
|||||||
@@ -834,7 +834,6 @@ impl ServerActor {
|
|||||||
closed by the server"
|
closed by the server"
|
||||||
);
|
);
|
||||||
self.session.record(SessionLoss::Displaced);
|
self.session.record(SessionLoss::Displaced);
|
||||||
self.connection_state = ConnectionState::Disconnected;
|
|
||||||
self.disconnect();
|
self.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,8 +844,8 @@ impl ServerActor {
|
|||||||
// it, so anything still waiting on the network is waiting for good.
|
// it, so anything still waiting on the network is waiting for good.
|
||||||
if matches!(self.connection_state, ConnectionState::Connected) {
|
if matches!(self.connection_state, ConnectionState::Connected) {
|
||||||
self.session.record(SessionLoss::Disconnected);
|
self.session.record(SessionLoss::Disconnected);
|
||||||
self.connection_state = ConnectionState::Disconnected;
|
|
||||||
}
|
}
|
||||||
|
self.connection_state = ConnectionState::Disconnected;
|
||||||
self.stream.take();
|
self.stream.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,6 +853,7 @@ impl ServerActor {
|
|||||||
debug!("[server] disconnected");
|
debug!("[server] disconnected");
|
||||||
|
|
||||||
self.stream.take();
|
self.stream.take();
|
||||||
|
self.connection_state = ConnectionState::Disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_connection_status(&mut self) {
|
fn check_connection_status(&mut self) {
|
||||||
@@ -939,13 +939,35 @@ impl Actor for ServerActor {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::post_login_messages;
|
use super::*;
|
||||||
use crate::message::Message;
|
|
||||||
|
|
||||||
fn code_of(message: &Message) -> u32 {
|
fn code_of(message: &Message) -> u32 {
|
||||||
u32::from_le_bytes(message.get_data()[0..4].try_into().unwrap())
|
u32::from_le_bytes(message.get_data()[0..4].try_into().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_timed_out_connect_parks_the_actor_in_disconnected() {
|
||||||
|
let (tx, _rx) = std::sync::mpsc::channel();
|
||||||
|
let mut actor = ServerActor::new(
|
||||||
|
PeerAddress::new("127.0.0.1".to_string(), 1),
|
||||||
|
tx,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
actor.connection_state = ConnectionState::Connecting {
|
||||||
|
since: Instant::now().checked_sub(Duration::from_secs(21)).unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
actor.tick();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(actor.connection_state, ConnectionState::Disconnected),
|
||||||
|
"a timed-out connect must leave Connecting"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn post_login_messages_carry_counts_and_conditional_wait_port() {
|
fn post_login_messages_carry_counts_and_conditional_wait_port() {
|
||||||
let messages = post_login_messages(true, 4321, 3, 7);
|
let messages = post_login_messages(true, 4321, 3, 7);
|
||||||
|
|||||||
@@ -50,10 +50,11 @@ impl Client {
|
|||||||
) = mpsc::channel();
|
) = mpsc::channel();
|
||||||
|
|
||||||
let mut ctx = self.context.write_safe()?;
|
let mut ctx = self.context.write_safe()?;
|
||||||
let peer_registry = PeerRegistry::new(
|
let peer_registry = PeerRegistry::with_capacity(
|
||||||
ctx.actor_system.clone(),
|
ctx.actor_system.clone(),
|
||||||
sender.clone(),
|
sender.clone(),
|
||||||
self.username.clone(),
|
self.username.clone(),
|
||||||
|
ctx.max_peers.clone(),
|
||||||
);
|
);
|
||||||
ctx.peer_registry = Some(peer_registry);
|
ctx.peer_registry = Some(peer_registry);
|
||||||
|
|
||||||
@@ -175,6 +176,13 @@ impl Client {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_max_peers(&self, max_peers: usize) {
|
||||||
|
if let Ok(ctx) = self.context.read_safe() {
|
||||||
|
ctx.max_peers
|
||||||
|
.store(max_peers.max(1), std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn remove_peer(&self, username: &str) {
|
pub fn remove_peer(&self, username: &str) {
|
||||||
let context = match self.context.read_safe() {
|
let context = match self.context.read_safe() {
|
||||||
@@ -206,6 +214,7 @@ impl Client {
|
|||||||
ConnectionType::P => {
|
ConnectionType::P => {
|
||||||
let username = peer.username;
|
let username = peer.username;
|
||||||
|
|
||||||
|
let refused = {
|
||||||
let context = match client_context.read_safe() {
|
let context = match client_context.read_safe() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -214,17 +223,27 @@ impl Client {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(ref registry) = context.peer_registry {
|
if let Some(ref registry) = context.peer_registry {
|
||||||
match registry.register_peer(peer_clone, stream, None) {
|
let protected = context.protected_peers();
|
||||||
Ok(_) => (),
|
match registry.register_peer_protected(
|
||||||
|
peer_clone, stream, None, &protected,
|
||||||
|
) {
|
||||||
|
Ok(_) => false,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!(
|
warn!(
|
||||||
"Failed to spawn peer actor for {:?}: {:?}",
|
"[client] refusing peer connection for \
|
||||||
|
{:?}: {:?}",
|
||||||
username, e
|
username, e
|
||||||
);
|
);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
trace!("PeerRegistry not initialized");
|
trace!("PeerRegistry not initialized");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if refused {
|
||||||
|
Self::fail_queued_downloads(&client_context, &username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use std::{
|
|||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
sync::{
|
sync::{
|
||||||
Arc, RwLock,
|
Arc, RwLock,
|
||||||
atomic::{AtomicBool, AtomicU32, Ordering},
|
atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering},
|
||||||
mpsc::{self, Receiver, Sender},
|
mpsc::{self, Receiver, Sender},
|
||||||
},
|
},
|
||||||
thread::{self, sleep},
|
thread::{self, sleep},
|
||||||
@@ -58,6 +58,10 @@ pub const DEFAULT_UPLOAD_SLOTS: usize = 10;
|
|||||||
/// before giving up and failing the download. Matches the direct-dial timeout.
|
/// before giving up and failing the download. Matches the direct-dial timeout.
|
||||||
const BROKER_CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
|
const BROKER_CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
|
|
||||||
|
const DEFAULT_MAX_PEERS: usize = 512;
|
||||||
|
|
||||||
|
const BROWSE_PROTECT_WINDOW: Duration = Duration::from_mins(5);
|
||||||
|
|
||||||
/// Source of non-zero correlation tokens for server-brokered connections.
|
/// Source of non-zero correlation tokens for server-brokered connections.
|
||||||
static NEXT_CONNECT_TOKEN: AtomicU32 = AtomicU32::new(1);
|
static NEXT_CONNECT_TOKEN: AtomicU32 = AtomicU32::new(1);
|
||||||
|
|
||||||
@@ -295,7 +299,8 @@ pub struct ClientContext {
|
|||||||
private_messages: Vec<UserMessage>,
|
private_messages: Vec<UserMessage>,
|
||||||
/// Correlation tokens for server-brokered (firewalled) connections, mapping
|
/// Correlation tokens for server-brokered (firewalled) connections, mapping
|
||||||
/// a token we sent in a ConnectToPeer to the peer we expect back.
|
/// a token we sent in a ConnectToPeer to the peer we expect back.
|
||||||
pending_connect_tokens: HashMap<u32, String>,
|
pending_connect_tokens: HashMap<u32, (String, Instant)>,
|
||||||
|
max_peers: Arc<AtomicUsize>,
|
||||||
/// Files we share with peers (read-only after connect).
|
/// Files we share with peers (read-only after connect).
|
||||||
pub shares: Arc<Shares>,
|
pub shares: Arc<Shares>,
|
||||||
/// The directories the current share index was built from.
|
/// The directories the current share index was built from.
|
||||||
@@ -311,6 +316,7 @@ pub struct ClientContext {
|
|||||||
pending_serves: HashMap<String, Vec<u32>>,
|
pending_serves: HashMap<String, Vec<u32>>,
|
||||||
/// Shared-file listings received from peers we browsed.
|
/// Shared-file listings received from peers we browsed.
|
||||||
browse_results: HashMap<String, Vec<SharedDirectory>>,
|
browse_results: HashMap<String, Vec<SharedDirectory>>,
|
||||||
|
pending_browses: HashMap<String, Instant>,
|
||||||
/// Latest snapshot of the public chat-room list (from `RoomList`, code 64).
|
/// Latest snapshot of the public chat-room list (from `RoomList`, code 64).
|
||||||
room_list: Vec<RoomInfo>,
|
room_list: Vec<RoomInfo>,
|
||||||
/// Chat-room events awaiting consumption by the client/UI.
|
/// Chat-room events awaiting consumption by the client/UI.
|
||||||
@@ -395,6 +401,7 @@ impl ClientContext {
|
|||||||
searches: HashMap::new(),
|
searches: HashMap::new(),
|
||||||
private_messages: Vec::new(),
|
private_messages: Vec::new(),
|
||||||
pending_connect_tokens: HashMap::new(),
|
pending_connect_tokens: HashMap::new(),
|
||||||
|
max_peers: Arc::new(AtomicUsize::new(DEFAULT_MAX_PEERS)),
|
||||||
shares: Arc::new(Shares::empty()),
|
shares: Arc::new(Shares::empty()),
|
||||||
shared_directories: Vec::new(),
|
shared_directories: Vec::new(),
|
||||||
peer_addresses: HashMap::new(),
|
peer_addresses: HashMap::new(),
|
||||||
@@ -403,6 +410,7 @@ impl ClientContext {
|
|||||||
active_uploads: HashMap::new(),
|
active_uploads: HashMap::new(),
|
||||||
pending_serves: HashMap::new(),
|
pending_serves: HashMap::new(),
|
||||||
browse_results: HashMap::new(),
|
browse_results: HashMap::new(),
|
||||||
|
pending_browses: HashMap::new(),
|
||||||
room_list: Vec::new(),
|
room_list: Vec::new(),
|
||||||
room_events: Vec::new(),
|
room_events: Vec::new(),
|
||||||
room_members: HashMap::new(),
|
room_members: HashMap::new(),
|
||||||
@@ -563,9 +571,17 @@ impl ClientContext {
|
|||||||
username: String,
|
username: String,
|
||||||
directories: Vec<SharedDirectory>,
|
directories: Vec<SharedDirectory>,
|
||||||
) {
|
) {
|
||||||
|
self.pending_browses.remove(&username);
|
||||||
self.browse_results.insert(username, directories);
|
self.browse_results.insert(username, directories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mark_browse_pending(&mut self, username: &str) {
|
||||||
|
let now = Instant::now();
|
||||||
|
self.pending_browses.retain(|_, deadline| *deadline > now);
|
||||||
|
self.pending_browses
|
||||||
|
.insert(username.to_string(), now + BROWSE_PROTECT_WINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove and return the shared-file listing browsed from `username`.
|
/// Remove and return the shared-file listing browsed from `username`.
|
||||||
pub fn take_browse_result(
|
pub fn take_browse_result(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -577,12 +593,60 @@ impl ClientContext {
|
|||||||
/// Remember that a server-brokered connection to `username` is pending under
|
/// Remember that a server-brokered connection to `username` is pending under
|
||||||
/// `token`; the peer will quote it back in a PierceFirewall.
|
/// `token`; the peer will quote it back in a PierceFirewall.
|
||||||
pub fn add_pending_connect(&mut self, token: u32, username: String) {
|
pub fn add_pending_connect(&mut self, token: u32, username: String) {
|
||||||
self.pending_connect_tokens.insert(token, username);
|
self.pending_connect_tokens
|
||||||
|
.insert(token, (username, Instant::now() + BROKER_CONNECT_TIMEOUT));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve and consume the peer expected for a brokered connection `token`.
|
/// Resolve and consume the peer expected for a brokered connection `token`.
|
||||||
pub fn take_pending_connect(&mut self, token: u32) -> Option<String> {
|
pub fn take_pending_connect(&mut self, token: u32) -> Option<String> {
|
||||||
self.pending_connect_tokens.remove(&token)
|
self.pending_connect_tokens
|
||||||
|
.remove(&token)
|
||||||
|
.map(|(username, _)| username)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_expired_connects(&mut self, now: Instant) -> Vec<String> {
|
||||||
|
let mut expired = Vec::new();
|
||||||
|
self.pending_connect_tokens
|
||||||
|
.retain(|_, (username, deadline)| {
|
||||||
|
if now >= *deadline {
|
||||||
|
expired.push(std::mem::take(username));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
expired
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn protected_peers(&self) -> HashSet<String> {
|
||||||
|
let mut protected: HashSet<String> = self
|
||||||
|
.downloads
|
||||||
|
.list()
|
||||||
|
.iter()
|
||||||
|
.filter(|d| {
|
||||||
|
matches!(
|
||||||
|
d.status,
|
||||||
|
DownloadStatus::Queued
|
||||||
|
| DownloadStatus::InProgress { .. }
|
||||||
|
| DownloadStatus::Paused { .. }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|d| d.username.clone())
|
||||||
|
.collect();
|
||||||
|
protected
|
||||||
|
.extend(self.active_uploads.values().map(|u| u.username.clone()));
|
||||||
|
protected.extend(self.uploads.values().map(|j| j.downloader.clone()));
|
||||||
|
protected
|
||||||
|
.extend(self.upload_queue.iter().map(|q| q.downloader.clone()));
|
||||||
|
protected.extend(self.pending_serves.keys().cloned());
|
||||||
|
protected.extend(self.pending_peer_messages.keys().cloned());
|
||||||
|
let now = Instant::now();
|
||||||
|
protected.extend(
|
||||||
|
self.pending_browses
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, deadline)| **deadline > now)
|
||||||
|
.map(|(username, _)| username.clone()),
|
||||||
|
);
|
||||||
|
protected
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record a private message received from another user.
|
/// Record a private message received from another user.
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use super::{
|
use super::{
|
||||||
Arc, BROKER_CONNECT_TIMEOUT, Client, ClientContext, ClientOperation,
|
Arc, Client, ClientContext, ClientOperation, ConnectionType, Download,
|
||||||
ConnectionType, Download, DownloadPeer, DownloadStatus, Peer, PeerMessage,
|
DownloadPeer, DownloadStatus, Duration, Instant, Peer, PeerMessage,
|
||||||
PeerRegistry, Receiver, RwLock, RwLockExt, ServerMessage,
|
PeerRegistry, Receiver, RwLock, RwLockExt, ServerMessage,
|
||||||
build_search_response, debug, error, info, next_connect_token, sleep,
|
build_search_response, debug, error, info, mpsc, next_connect_token,
|
||||||
thread, trace, warn,
|
thread, trace, warn,
|
||||||
};
|
};
|
||||||
use crate::message::server::MessageFactory;
|
use crate::message::server::MessageFactory;
|
||||||
|
|
||||||
|
const CONNECT_SWEEP_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub(crate) fn listen_to_client_operations(
|
pub(crate) fn listen_to_client_operations(
|
||||||
reader: Receiver<ClientOperation>,
|
reader: Receiver<ClientOperation>,
|
||||||
@@ -14,9 +16,21 @@ impl Client {
|
|||||||
own_username: String,
|
own_username: String,
|
||||||
) {
|
) {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
for operation in reader {
|
let mut last_sweep = Instant::now();
|
||||||
|
loop {
|
||||||
|
let next = reader.recv_timeout(CONNECT_SWEEP_INTERVAL);
|
||||||
|
if last_sweep.elapsed() >= CONNECT_SWEEP_INTERVAL {
|
||||||
|
last_sweep = Instant::now();
|
||||||
|
Self::sweep_expired_connects(&client_context);
|
||||||
|
}
|
||||||
|
let operation = match next {
|
||||||
|
Ok(operation) => operation,
|
||||||
|
Err(mpsc::RecvTimeoutError::Timeout) => continue,
|
||||||
|
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||||
|
};
|
||||||
match operation {
|
match operation {
|
||||||
ClientOperation::ConnectToPeer(peer) => {
|
ClientOperation::ConnectToPeer(peer) => {
|
||||||
|
if matches!(peer.connection_type, ConnectionType::F) {
|
||||||
let client_context_clone = client_context.clone();
|
let client_context_clone = client_context.clone();
|
||||||
let own_username_clone = own_username.clone();
|
let own_username_clone = own_username.clone();
|
||||||
|
|
||||||
@@ -28,6 +42,14 @@ impl Client {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
Self::connect_to_peer(
|
||||||
|
peer,
|
||||||
|
client_context.clone(),
|
||||||
|
own_username.clone(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ClientOperation::SearchResult(search_result) => {
|
ClientOperation::SearchResult(search_result) => {
|
||||||
trace!("[client] SearchResult {:?}", search_result);
|
trace!("[client] SearchResult {:?}", search_result);
|
||||||
@@ -122,6 +144,28 @@ impl Client {
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
if matches!(
|
||||||
|
download.status,
|
||||||
|
DownloadStatus::InProgress { .. }
|
||||||
|
| DownloadStatus::Completed
|
||||||
|
) {
|
||||||
|
debug!(
|
||||||
|
"[client] transfer token {} already claimed; \
|
||||||
|
ignoring replayed TransferResponse",
|
||||||
|
token
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(mut ctx) = client_context.write_safe() {
|
||||||
|
ctx.update_download_with_status(
|
||||||
|
token,
|
||||||
|
DownloadStatus::InProgress {
|
||||||
|
bytes_downloaded: 0,
|
||||||
|
total_bytes: download.size,
|
||||||
|
speed_bytes_per_sec: 0.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let download_peer = DownloadPeer::new(
|
let download_peer = DownloadPeer::new(
|
||||||
@@ -285,17 +329,12 @@ impl Client {
|
|||||||
u8::try_from(obfuscation_type).unwrap_or(0),
|
u8::try_from(obfuscation_type).unwrap_or(0),
|
||||||
obfuscated_port,
|
obfuscated_port,
|
||||||
);
|
);
|
||||||
let client_context_clone = client_context.clone();
|
|
||||||
let own_username_clone = own_username.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
Self::connect_to_peer(
|
Self::connect_to_peer(
|
||||||
peer,
|
peer,
|
||||||
client_context_clone,
|
client_context.clone(),
|
||||||
own_username_clone,
|
own_username.clone(),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClientOperation::UpdateDownloadTokens(
|
ClientOperation::UpdateDownloadTokens(
|
||||||
@@ -751,30 +790,19 @@ impl Client {
|
|||||||
ConnectionType::P,
|
ConnectionType::P,
|
||||||
);
|
);
|
||||||
let _ = sender.send(ServerMessage::SendMessage(msg));
|
let _ = sender.send(ServerMessage::SendMessage(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Bound the brokered attempt: if no PierceFirewall
|
fn sweep_expired_connects(client_context: &Arc<RwLock<ClientContext>>) {
|
||||||
// consumes the token, fail the peer's queued
|
let expired = match client_context.write_safe() {
|
||||||
// downloads (so the caller's Receiver unblocks)
|
Ok(mut ctx) => ctx.take_expired_connects(Instant::now()),
|
||||||
// and reclaim the token. A successful pierce
|
Err(_) => return,
|
||||||
// takes the token first, making this a no-op.
|
};
|
||||||
let timeout_ctx = client_context.clone();
|
for username in expired {
|
||||||
let timeout_user = username.clone();
|
Self::fail_queued_downloads(client_context, &username);
|
||||||
thread::spawn(move || {
|
|
||||||
sleep(BROKER_CONNECT_TIMEOUT);
|
|
||||||
let still_pending =
|
|
||||||
timeout_ctx.write_safe().is_ok_and(|mut c| {
|
|
||||||
c.take_pending_connect(token).is_some()
|
|
||||||
});
|
|
||||||
if still_pending {
|
|
||||||
Self::fail_queued_downloads(
|
|
||||||
&timeout_ctx,
|
|
||||||
&timeout_user,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -177,7 +177,8 @@ impl Client {
|
|||||||
let request =
|
let request =
|
||||||
crate::message::server::MessageFactory::build_get_share_file_list();
|
crate::message::server::MessageFactory::build_get_share_file_list();
|
||||||
let (connected, registry) = {
|
let (connected, registry) = {
|
||||||
let ctx = self.context.read_safe()?;
|
let mut ctx = self.context.write_safe()?;
|
||||||
|
ctx.mark_browse_pending(username);
|
||||||
(
|
(
|
||||||
ctx.peer_registry
|
ctx.peer_registry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
@@ -479,3 +479,146 @@ fn a_clean_disconnect_keeps_queued_uploads_an_error_drops_them() {
|
|||||||
thread::sleep(Duration::from_millis(10));
|
thread::sleep(Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_pending_connect_expires_after_the_broker_timeout() {
|
||||||
|
let mut context = ClientContext::new();
|
||||||
|
context.add_pending_connect(7, "ghost".to_string());
|
||||||
|
|
||||||
|
assert!(context.take_expired_connects(Instant::now()).is_empty());
|
||||||
|
|
||||||
|
let after_deadline =
|
||||||
|
Instant::now() + BROKER_CONNECT_TIMEOUT + Duration::from_secs(1);
|
||||||
|
assert_eq!(
|
||||||
|
context.take_expired_connects(after_deadline),
|
||||||
|
vec!["ghost".to_string()]
|
||||||
|
);
|
||||||
|
assert!(context.take_pending_connect(7).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protected_peers_covers_downloads_uploads_and_pending_serves() {
|
||||||
|
let mut context = ClientContext::new();
|
||||||
|
context.add_download(download(
|
||||||
|
"downloader",
|
||||||
|
"song.mp3",
|
||||||
|
1,
|
||||||
|
DownloadStatus::Queued,
|
||||||
|
mpsc::channel().0,
|
||||||
|
));
|
||||||
|
context.add_download(download(
|
||||||
|
"done",
|
||||||
|
"old.mp3",
|
||||||
|
2,
|
||||||
|
DownloadStatus::Completed,
|
||||||
|
mpsc::channel().0,
|
||||||
|
));
|
||||||
|
context
|
||||||
|
.pending_serves
|
||||||
|
.insert("waiting".to_string(), vec![9]);
|
||||||
|
context.mark_browse_pending("browsed");
|
||||||
|
|
||||||
|
let protected = context.protected_peers();
|
||||||
|
assert!(protected.contains("downloader"));
|
||||||
|
assert!(protected.contains("waiting"));
|
||||||
|
assert!(protected.contains("browsed"));
|
||||||
|
assert!(!protected.contains("done"));
|
||||||
|
|
||||||
|
context.store_browse_result("browsed".to_string(), Vec::new());
|
||||||
|
assert!(!context.protected_peers().contains("browsed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_expired_broker_connect_fails_the_queued_downloads() {
|
||||||
|
let client = Client::new("u", "p");
|
||||||
|
let (sender, receiver) = mpsc::channel();
|
||||||
|
{
|
||||||
|
let mut ctx = client.context.write().unwrap();
|
||||||
|
ctx.add_download(download(
|
||||||
|
"ghost",
|
||||||
|
"f.mp3",
|
||||||
|
7,
|
||||||
|
DownloadStatus::Queued,
|
||||||
|
sender,
|
||||||
|
));
|
||||||
|
ctx.pending_connect_tokens
|
||||||
|
.insert(7, ("ghost".to_string(), Instant::now()));
|
||||||
|
}
|
||||||
|
let (_ops_tx, ops_rx) = mpsc::channel();
|
||||||
|
Client::listen_to_client_operations(
|
||||||
|
ops_rx,
|
||||||
|
client.context,
|
||||||
|
"u".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let status = receiver.recv_timeout(Duration::from_secs(5));
|
||||||
|
assert!(
|
||||||
|
matches!(status, Ok(DownloadStatus::Failed(_))),
|
||||||
|
"the sweep must fail the queued download, got {status:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_replayed_transfer_response_does_not_start_a_second_transfer() {
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
listener.set_nonblocking(true).unwrap();
|
||||||
|
let port = u32::from(listener.local_addr().unwrap().port());
|
||||||
|
|
||||||
|
let client = Client::new("u", "p");
|
||||||
|
let (sender, _receiver) = mpsc::channel();
|
||||||
|
client.context.write().unwrap().add_download(download(
|
||||||
|
"peer",
|
||||||
|
"f.mp3",
|
||||||
|
9,
|
||||||
|
DownloadStatus::Queued,
|
||||||
|
sender,
|
||||||
|
));
|
||||||
|
|
||||||
|
let (ops_tx, ops_rx) = mpsc::channel();
|
||||||
|
Client::listen_to_client_operations(
|
||||||
|
ops_rx,
|
||||||
|
client.context,
|
||||||
|
"u".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let peer = Peer::new(
|
||||||
|
"peer".to_string(),
|
||||||
|
ConnectionType::F,
|
||||||
|
"127.0.0.1".to_string(),
|
||||||
|
port,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
ops_tx
|
||||||
|
.send(ClientOperation::DownloadFromPeer(9, peer.clone(), true))
|
||||||
|
.unwrap();
|
||||||
|
ops_tx
|
||||||
|
.send(ClientOperation::DownloadFromPeer(9, peer, true))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
let mut connections = 0;
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
match listener.accept() {
|
||||||
|
Ok(_) => {
|
||||||
|
connections += 1;
|
||||||
|
if connections == 1 {
|
||||||
|
thread::sleep(Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
if connections >= 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
connections, 1,
|
||||||
|
"a replayed TransferResponse must not dial the peer again"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod queue_upload;
|
|||||||
mod shared_file_list;
|
mod shared_file_list;
|
||||||
mod transfer_request;
|
mod transfer_request;
|
||||||
mod transfer_response;
|
mod transfer_response;
|
||||||
|
mod upload_denied;
|
||||||
mod upload_failed;
|
mod upload_failed;
|
||||||
|
|
||||||
// Re-export handlers
|
// Re-export handlers
|
||||||
@@ -24,4 +25,5 @@ pub use shared_file_list::{
|
|||||||
};
|
};
|
||||||
pub use transfer_request::TransferRequest;
|
pub use transfer_request::TransferRequest;
|
||||||
pub use transfer_response::TransferResponse;
|
pub use transfer_response::TransferResponse;
|
||||||
|
pub use upload_denied::UploadDeniedHandler;
|
||||||
pub use upload_failed::UploadFailedHandler;
|
pub use upload_failed::UploadFailedHandler;
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
use crate::info;
|
||||||
|
use crate::{
|
||||||
|
message::{Message, MessageHandler},
|
||||||
|
peer::PeerMessage,
|
||||||
|
};
|
||||||
|
use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
|
pub struct UploadDeniedHandler;
|
||||||
|
impl MessageHandler<PeerMessage> for UploadDeniedHandler {
|
||||||
|
fn get_code(&self) -> u8 {
|
||||||
|
50
|
||||||
|
}
|
||||||
|
fn handle(&self, message: &mut Message, sender: Sender<PeerMessage>) {
|
||||||
|
let filename = message.read_string();
|
||||||
|
let reason = message.read_string();
|
||||||
|
info!("Upload denied for {}: {}", filename, reason);
|
||||||
|
if reason == "Queued" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = sender.send(PeerMessage::UploadFailed(String::new(), filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::message::framed;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_queued_reason_is_not_a_failure() {
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
let mut message = framed(|m| {
|
||||||
|
m.write_string("@@share\\gone.mp3").write_string("Queued");
|
||||||
|
});
|
||||||
|
|
||||||
|
UploadDeniedHandler.handle(&mut message, tx);
|
||||||
|
assert!(rx.try_recv().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_denied_filename_is_forwarded() {
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
let mut message = framed(|m| {
|
||||||
|
m.write_string("@@share\\gone.mp3")
|
||||||
|
.write_string("File not shared.");
|
||||||
|
});
|
||||||
|
|
||||||
|
UploadDeniedHandler.handle(&mut message, tx);
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(PeerMessage::UploadFailed(_, filename)) => {
|
||||||
|
assert_eq!(filename, "@@share\\gone.mp3");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,31 @@ impl MessageHandler<PeerMessage> for UploadFailedHandler {
|
|||||||
fn get_code(&self) -> u8 {
|
fn get_code(&self) -> u8 {
|
||||||
46
|
46
|
||||||
}
|
}
|
||||||
fn handle(&self, message: &mut Message, _sender: Sender<PeerMessage>) {
|
fn handle(&self, message: &mut Message, sender: Sender<PeerMessage>) {
|
||||||
let filename = message.read_string();
|
let filename = message.read_string();
|
||||||
info!("Upload failed for {}", filename);
|
info!("Upload failed for {}", filename);
|
||||||
|
let _ = sender.send(PeerMessage::UploadFailed(String::new(), filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::message::framed;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_failed_filename_is_forwarded() {
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
let mut message = framed(|m| {
|
||||||
|
m.write_string("@@share\\gone.mp3");
|
||||||
|
});
|
||||||
|
|
||||||
|
UploadFailedHandler.handle(&mut message, tx);
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(PeerMessage::UploadFailed(_, filename)) => {
|
||||||
|
assert_eq!(filename, "@@share\\gone.mp3");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected: {other:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,14 +484,6 @@ impl DownloadPeer {
|
|||||||
self.no_pierce
|
self.no_pierce
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(ref dl) = download {
|
|
||||||
let _ = dl.sender.send(DownloadStatus::Queued);
|
|
||||||
client_context
|
|
||||||
.write()
|
|
||||||
.map_err(|_| DownloadError::LockPoisoned)?
|
|
||||||
.update_download_with_status(dl.token, DownloadStatus::Queued);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut stream = match stream {
|
let mut stream = match stream {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
Self::apply_transfer_deadlines(&s)?;
|
Self::apply_transfer_deadlines(&s)?;
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ use std::net::{TcpListener, TcpStream};
|
|||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::client::{ClientContext, ClientOperation};
|
use crate::client::{Client, ClientContext, ClientOperation};
|
||||||
|
|
||||||
use crate::message::{Message, MessageReader};
|
use crate::message::{Message, MessageReader};
|
||||||
use crate::peer::{ConnectionType, DownloadPeer, Peer};
|
use crate::peer::{ConnectionType, DownloadPeer, Peer};
|
||||||
use crate::types::Download;
|
use crate::types::Download;
|
||||||
use crate::utils::lock::RwLockExt;
|
use crate::utils::lock::RwLockExt;
|
||||||
use crate::{DownloadStatus, debug, error, info, trace};
|
use crate::utils::semaphore::{Permit, Semaphore};
|
||||||
|
use crate::{DownloadStatus, debug, error, info, trace, warn};
|
||||||
|
|
||||||
/// How long to wait before accepting again after a failure.
|
/// How long to wait before accepting again after a failure.
|
||||||
///
|
///
|
||||||
@@ -25,6 +26,10 @@ const PEER_INIT_MESSAGE_CODE: u8 = 1;
|
|||||||
/// How long an accepted peer gets to send its peer-init handshake.
|
/// How long an accepted peer gets to send its peer-init handshake.
|
||||||
const PEER_INIT_TIMEOUT: Duration = Duration::from_secs(10);
|
const PEER_INIT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
const MAX_HANDSHAKES: usize = 128;
|
||||||
|
|
||||||
|
const MAX_PEER_INIT_BYTES: usize = 4096;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct ConnectionContext {
|
struct ConnectionContext {
|
||||||
client_sender: Sender<ClientOperation>,
|
client_sender: Sender<ClientOperation>,
|
||||||
@@ -41,16 +46,30 @@ struct PeerInitData {
|
|||||||
fn read_peer_init_message(
|
fn read_peer_init_message(
|
||||||
stream: &mut TcpStream,
|
stream: &mut TcpStream,
|
||||||
reader: &mut MessageReader,
|
reader: &mut MessageReader,
|
||||||
|
deadline: Instant,
|
||||||
) -> io::Result<Message> {
|
) -> io::Result<Message> {
|
||||||
// An untrusted peer gets a bounded handshake. Without this a peer that
|
// An untrusted peer gets a bounded handshake. Without this a peer that
|
||||||
// connects and stays silent parks this read forever, pinning a thread that
|
// connects and stays silent parks this read forever, pinning a thread that
|
||||||
// owes us a peer init.
|
// owes us a peer init.
|
||||||
stream.set_read_timeout(Some(PEER_INIT_TIMEOUT))?;
|
|
||||||
let message = loop {
|
let message = loop {
|
||||||
|
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||||
|
if remaining.is_zero() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
"peer init deadline passed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
stream.set_read_timeout(Some(remaining))?;
|
||||||
reader.read_from_socket(stream)?;
|
reader.read_from_socket(stream)?;
|
||||||
if let Ok(Some(msg)) = reader.extract_message() {
|
if let Ok(Some(msg)) = reader.extract_message() {
|
||||||
break msg;
|
break msg;
|
||||||
}
|
}
|
||||||
|
if reader.buffer_len() > MAX_PEER_INIT_BYTES {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"peer init larger than any handshake",
|
||||||
|
));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Whatever this connection turns into reads on its own terms from here.
|
// Whatever this connection turns into reads on its own terms from here.
|
||||||
@@ -119,7 +138,7 @@ fn handle_peer_connection(
|
|||||||
stream: TcpStream,
|
stream: TcpStream,
|
||||||
reader: MessageReader,
|
reader: MessageReader,
|
||||||
context: &ConnectionContext,
|
context: &ConnectionContext,
|
||||||
) {
|
) -> bool {
|
||||||
// The peer actor multiplexes socket reads with its mailbox on a single
|
// The peer actor multiplexes socket reads with its mailbox on a single
|
||||||
// thread: `tick()` reads the socket, but outgoing messages (e.g. a queued
|
// thread: `tick()` reads the socket, but outgoing messages (e.g. a queued
|
||||||
// QueueUpload for a download) are delivered through the mailbox between
|
// QueueUpload for a download) are delivered through the mailbox between
|
||||||
@@ -128,30 +147,44 @@ fn handle_peer_connection(
|
|||||||
// outbound path and drive this connection non-blocking.
|
// outbound path and drive this connection non-blocking.
|
||||||
if let Err(e) = stream.set_nonblocking(true) {
|
if let Err(e) = stream.set_nonblocking(true) {
|
||||||
error!("[listener] failed to set peer stream non-blocking: {}", e);
|
error!("[listener] failed to set peer stream non-blocking: {}", e);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
stream.set_nodelay(true).ok();
|
stream.set_nodelay(true).ok();
|
||||||
|
|
||||||
|
let registered = {
|
||||||
let client_context = match context.client_context.read_safe() {
|
let client_context = match context.client_context.read_safe() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("[listener] handle_peer_connection lock: {}", e);
|
error!("[listener] handle_peer_connection lock: {}", e);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(ref registry) = client_context.peer_registry {
|
if let Some(ref registry) = client_context.peer_registry {
|
||||||
match registry.register_peer(peer.clone(), Some(stream), Some(reader)) {
|
let protected = client_context.protected_peers();
|
||||||
Ok(_) => (),
|
match registry.register_peer_protected(
|
||||||
|
peer.clone(),
|
||||||
|
Some(stream),
|
||||||
|
Some(reader),
|
||||||
|
&protected,
|
||||||
|
) {
|
||||||
|
Ok(_) => true,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
warn!(
|
||||||
"Failed to spawn peer actor for {:?}: {:?}",
|
"[listener] refusing peer connection for {:?}: {:?}",
|
||||||
peer.username, e
|
peer.username, e
|
||||||
);
|
);
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("PeerRegistry not initialized");
|
error!("PeerRegistry not initialized");
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if !registered {
|
||||||
|
Client::fail_queued_downloads(&context.client_context, &peer.username);
|
||||||
|
}
|
||||||
|
registered
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_file_connection(
|
fn handle_file_connection(
|
||||||
@@ -271,18 +304,19 @@ fn handle_pierce_firewall(
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
handle_peer_connection(peer, stream, reader, context);
|
if handle_peer_connection(peer, stream, reader, context) {
|
||||||
|
|
||||||
// Inbound peers don't self-announce, so nudge the client to flush any
|
// Inbound peers don't self-announce, so nudge the client to flush any
|
||||||
// downloads queued for this now-connected peer.
|
// downloads queued for this now-connected peer.
|
||||||
let _ = context
|
let _ = context
|
||||||
.client_sender
|
.client_sender
|
||||||
.send(ClientOperation::PeerConnected(username));
|
.send(ClientOperation::PeerConnected(username));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_incoming_connection(
|
fn handle_incoming_connection(
|
||||||
mut stream: TcpStream,
|
mut stream: TcpStream,
|
||||||
context: ConnectionContext,
|
context: ConnectionContext,
|
||||||
|
permit: Permit,
|
||||||
) {
|
) {
|
||||||
let Ok(peer_addr) = stream.peer_addr() else {
|
let Ok(peer_addr) = stream.peer_addr() else {
|
||||||
error!("[listener] failed to get peer address");
|
error!("[listener] failed to get peer address");
|
||||||
@@ -294,7 +328,9 @@ fn handle_incoming_connection(
|
|||||||
let mut reader = MessageReader::new();
|
let mut reader = MessageReader::new();
|
||||||
|
|
||||||
// A peer that dials and then goes away is routine, not an error.
|
// A peer that dials and then goes away is routine, not an error.
|
||||||
let message = match read_peer_init_message(&mut stream, &mut reader) {
|
let deadline = Instant::now() + PEER_INIT_TIMEOUT;
|
||||||
|
let message =
|
||||||
|
match read_peer_init_message(&mut stream, &mut reader, deadline) {
|
||||||
Ok(message) => message,
|
Ok(message) => message,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!(
|
debug!(
|
||||||
@@ -303,6 +339,7 @@ fn handle_incoming_connection(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
drop(permit);
|
||||||
|
|
||||||
// A firewalled peer brokered through the server connects back with a
|
// A firewalled peer brokered through the server connects back with a
|
||||||
// PierceFirewall (code 0) instead of a PeerInit (code 1).
|
// PierceFirewall (code 0) instead of a PeerInit (code 1).
|
||||||
@@ -343,14 +380,15 @@ fn handle_incoming_connection(
|
|||||||
|
|
||||||
match init_data.connection_type {
|
match init_data.connection_type {
|
||||||
ConnectionType::P => {
|
ConnectionType::P => {
|
||||||
handle_peer_connection(peer, stream, reader, &context);
|
if handle_peer_connection(peer, stream, reader, &context) {
|
||||||
// Inbound peers don't self-announce, so nudge the client to flush
|
// Inbound peers don't self-announce, so nudge the client to
|
||||||
// anything queued for this now-connected peer — same as the
|
// flush anything queued for this now-connected peer — same as
|
||||||
// PierceFirewall path.
|
// the PierceFirewall path.
|
||||||
let _ = context
|
let _ = context
|
||||||
.client_sender
|
.client_sender
|
||||||
.send(ClientOperation::PeerConnected(init_data.username));
|
.send(ClientOperation::PeerConnected(init_data.username));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConnectionType::F => handle_file_connection(
|
ConnectionType::F => handle_file_connection(
|
||||||
peer,
|
peer,
|
||||||
@@ -406,6 +444,7 @@ impl Listen {
|
|||||||
client_context,
|
client_context,
|
||||||
own_username,
|
own_username,
|
||||||
};
|
};
|
||||||
|
let handshakes = Arc::new(Semaphore::new(MAX_HANDSHAKES));
|
||||||
|
|
||||||
for stream in listener.incoming() {
|
for stream in listener.incoming() {
|
||||||
let Ok(stream) = stream else {
|
let Ok(stream) = stream else {
|
||||||
@@ -421,11 +460,79 @@ impl Listen {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let permit = handshakes.acquire();
|
||||||
let context = context.clone();
|
let context = context.clone();
|
||||||
// One thread per connection: the peer-init handshake blocks, and a
|
// One thread per connection: the peer-init handshake blocks, and a
|
||||||
// peer that is slow to send one must not stop us accepting anybody
|
// peer that is slow to send one must not stop us accepting anybody
|
||||||
// else — a wedged accept loop makes us unreachable to every peer.
|
// else — a wedged accept loop makes us unreachable to every peer.
|
||||||
thread::spawn(move || handle_incoming_connection(stream, context));
|
thread::spawn(move || {
|
||||||
|
handle_incoming_connection(stream, context, permit);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::net::TcpListener;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_dribbling_handshake_hits_the_total_deadline() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
let writer = std::thread::spawn(move || {
|
||||||
|
let mut socket = TcpStream::connect(addr).unwrap();
|
||||||
|
let _ = socket.write_all(&1000u32.to_le_bytes());
|
||||||
|
for _ in 0..20 {
|
||||||
|
if socket.write_all(&[0u8]).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
|
let mut reader = MessageReader::new();
|
||||||
|
|
||||||
|
let started = Instant::now();
|
||||||
|
let result = read_peer_init_message(
|
||||||
|
&mut stream,
|
||||||
|
&mut reader,
|
||||||
|
started + Duration::from_millis(300),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err(), "a dribbled handshake must not succeed");
|
||||||
|
assert!(
|
||||||
|
started.elapsed() < Duration::from_secs(5),
|
||||||
|
"the deadline must bound the whole handshake, not each read"
|
||||||
|
);
|
||||||
|
let _ = writer.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_oversized_handshake_is_rejected() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
let writer = std::thread::spawn(move || {
|
||||||
|
let mut socket = TcpStream::connect(addr).unwrap();
|
||||||
|
let junk = vec![0xFFu8; MAX_PEER_INIT_BYTES * 2];
|
||||||
|
let _ = socket.write_all(&junk);
|
||||||
|
std::thread::sleep(Duration::from_millis(500));
|
||||||
|
});
|
||||||
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
|
let mut reader = MessageReader::new();
|
||||||
|
|
||||||
|
let result = read_peer_init_message(
|
||||||
|
&mut stream,
|
||||||
|
&mut reader,
|
||||||
|
Instant::now() + Duration::from_secs(5),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"an oversized handshake must be rejected, got {result:?}"
|
||||||
|
);
|
||||||
|
let _ = writer.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod logger;
|
|||||||
pub mod lock;
|
pub mod lock;
|
||||||
pub mod md5;
|
pub mod md5;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
|
pub(crate) mod semaphore;
|
||||||
pub mod zlib;
|
pub mod zlib;
|
||||||
|
|
||||||
// Re-export commonly used items
|
// Re-export commonly used items
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
use std::sync::{Arc, Condvar, Mutex, PoisonError};
|
||||||
|
|
||||||
|
pub struct Semaphore {
|
||||||
|
permits: Mutex<usize>,
|
||||||
|
available: Condvar,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Permit(Arc<Semaphore>);
|
||||||
|
|
||||||
|
impl Semaphore {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(permits: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
permits: Mutex::new(permits),
|
||||||
|
available: Condvar::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn acquire(self: &Arc<Self>) -> Permit {
|
||||||
|
let mut permits =
|
||||||
|
self.permits.lock().unwrap_or_else(PoisonError::into_inner);
|
||||||
|
while *permits == 0 {
|
||||||
|
permits = self
|
||||||
|
.available
|
||||||
|
.wait(permits)
|
||||||
|
.unwrap_or_else(PoisonError::into_inner);
|
||||||
|
}
|
||||||
|
*permits -= 1;
|
||||||
|
Permit(Arc::clone(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Permit {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut permits = self
|
||||||
|
.0
|
||||||
|
.permits
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(PoisonError::into_inner);
|
||||||
|
*permits += 1;
|
||||||
|
self.0.available.notify_one();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn permits_bound_concurrent_holders() {
|
||||||
|
let semaphore = Arc::new(Semaphore::new(2));
|
||||||
|
let first = semaphore.acquire();
|
||||||
|
let _second = semaphore.acquire();
|
||||||
|
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _third = semaphore.acquire();
|
||||||
|
tx.send(()).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rx.recv_timeout(Duration::from_millis(100)).is_err(),
|
||||||
|
"a third acquire must block while both permits are held"
|
||||||
|
);
|
||||||
|
drop(first);
|
||||||
|
assert!(
|
||||||
|
rx.recv_timeout(Duration::from_secs(2)).is_ok(),
|
||||||
|
"releasing a permit must wake the waiter"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -565,6 +565,29 @@ fn run_mock_uploader(cfg: &MockUpload) -> std::io::Result<u64> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_refusing_uploader(
|
||||||
|
cfg: &MockUpload,
|
||||||
|
refusal: &Message,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let mut p = connect_retry(&cfg.listen_addr, Duration::from_secs(5))?;
|
||||||
|
p.set_read_timeout(Some(Duration::from_secs(10)))?;
|
||||||
|
p.write_all(&peer_init_bytes(&cfg.peer_username, "P", 0))?;
|
||||||
|
p.flush()?;
|
||||||
|
let _ = cfg.ready.send(());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let msg = read_framed(&mut p)?;
|
||||||
|
if msg.get_message_code() == 43 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.write_all(&refusal.get_buffer())?;
|
||||||
|
p.flush()?;
|
||||||
|
std::thread::sleep(Duration::from_millis(500));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Open an F (file transfer) connection to `downloader_addr` and stream
|
/// Open an F (file transfer) connection to `downloader_addr` and stream
|
||||||
/// `content`. PeerInit(F) and the 4-byte transfer token are written together so
|
/// `content`. PeerInit(F) and the 4-byte transfer token are written together so
|
||||||
/// the token lands in the listener's read buffer, where the download is looked
|
/// the token lands in the listener's read buffer, where the download is looked
|
||||||
@@ -760,6 +783,94 @@ fn a_file_downloads_from_a_peer_over_p_and_f_connections() {
|
|||||||
let _ = std::fs::remove_dir_all(&download_dir);
|
let _ = std::fs::remove_dir_all(&download_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn refusal_fails_the_download_quickly(
|
||||||
|
peer_username: &str,
|
||||||
|
build_refusal: impl FnOnce(&str) -> Message,
|
||||||
|
) {
|
||||||
|
let server = server_or_skip!();
|
||||||
|
|
||||||
|
let listen_port = free_port().expect("free listen port");
|
||||||
|
let mut client = Client::with_settings(server.listening_settings(
|
||||||
|
&format!("{peer_username}_dl"),
|
||||||
|
"pw",
|
||||||
|
listen_port,
|
||||||
|
));
|
||||||
|
client.connect().expect("connect");
|
||||||
|
assert!(client.login().expect("login"));
|
||||||
|
|
||||||
|
let filename = "gone_song.mp3";
|
||||||
|
let refusal = build_refusal(filename);
|
||||||
|
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
|
||||||
|
let cfg = MockUpload {
|
||||||
|
listen_addr: format!("127.0.0.1:{listen_port}"),
|
||||||
|
peer_username: peer_username.to_string(),
|
||||||
|
filename: filename.to_string(),
|
||||||
|
content: Vec::new(),
|
||||||
|
token: 0,
|
||||||
|
ready: ready_tx,
|
||||||
|
};
|
||||||
|
let uploader = std::thread::spawn(move || {
|
||||||
|
if let Err(e) = run_refusing_uploader(&cfg, &refusal) {
|
||||||
|
eprintln!("[mock refusing uploader] {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ready_rx
|
||||||
|
.recv_timeout(Duration::from_secs(5))
|
||||||
|
.expect("mock uploader P connection");
|
||||||
|
std::thread::sleep(Duration::from_millis(1500));
|
||||||
|
|
||||||
|
let download_dir = unique_download_dir();
|
||||||
|
let (_download, status_rx) = client
|
||||||
|
.download(
|
||||||
|
filename.to_string(),
|
||||||
|
peer_username.to_string(),
|
||||||
|
0,
|
||||||
|
download_dir.display().to_string(),
|
||||||
|
)
|
||||||
|
.expect("start download");
|
||||||
|
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(10);
|
||||||
|
let mut failed = false;
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
match status_rx.recv_timeout(Duration::from_millis(500)) {
|
||||||
|
Ok(DownloadStatus::Failed(_)) => {
|
||||||
|
failed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(DownloadStatus::Completed) => {
|
||||||
|
panic!("the refused download must not complete")
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = uploader.join();
|
||||||
|
let _ = std::fs::remove_dir_all(&download_dir);
|
||||||
|
|
||||||
|
assert!(failed, "the peer's refusal should fail the download fast");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_upload_failed_reply_fails_the_download_quickly() {
|
||||||
|
refusal_fails_the_download_quickly("e2e_failing_peer", |filename| {
|
||||||
|
let mut refusal = Message::new();
|
||||||
|
refusal.write_int32(46).write_string(filename);
|
||||||
|
refusal
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_upload_denied_reply_fails_the_download_quickly() {
|
||||||
|
refusal_fails_the_download_quickly("e2e_denying_peer", |filename| {
|
||||||
|
let mut refusal = Message::new();
|
||||||
|
refusal
|
||||||
|
.write_int32(50)
|
||||||
|
.write_string(filename)
|
||||||
|
.write_string("File not shared.");
|
||||||
|
refusal
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_interrupted_download_resumes_from_its_partial_file() {
|
fn an_interrupted_download_resumes_from_its_partial_file() {
|
||||||
let server = server_or_skip!();
|
let server = server_or_skip!();
|
||||||
|
|||||||
Reference in New Issue
Block a user