2026-05-26 06:24:57 +00:00
|
|
|
use crate::io::ErrorKind;
|
|
|
|
|
use crate::log::warn;
|
2026-05-24 17:56:57 +00:00
|
|
|
use crate::records::memory::connections::get_client_type_from_memory;
|
|
|
|
|
use crate::records::memory::enums::ClientType;
|
2026-06-01 14:29:11 +00:00
|
|
|
use crate::rpc::command_maps::{
|
|
|
|
|
RPC_BLOCK_HEIGHT, RPC_BLOCK_PIECE, RPC_REPLY, RPC_TORRENT_BY_HEIGHT,
|
|
|
|
|
};
|
2026-05-24 17:56:57 +00:00
|
|
|
use crate::rpc::server::connection_memory_manager::remove_stream_from_memory;
|
|
|
|
|
use crate::rpc::server::flood_protection::check_request_frequency_with_client_type;
|
|
|
|
|
use crate::rpc::server::structs::IncomingCommand;
|
|
|
|
|
use crate::sled::Db;
|
2026-05-26 06:24:57 +00:00
|
|
|
use crate::sleep;
|
2026-06-01 14:29:11 +00:00
|
|
|
use crate::wallets::structures::Wallet;
|
2026-05-24 17:56:57 +00:00
|
|
|
use crate::Arc;
|
|
|
|
|
use crate::Duration;
|
|
|
|
|
use crate::Mutex;
|
|
|
|
|
use crate::TcpStream;
|
|
|
|
|
|
2026-05-26 06:24:57 +00:00
|
|
|
async fn read_next_command_byte(
|
|
|
|
|
stream_locked: &Arc<Mutex<TcpStream>>,
|
|
|
|
|
) -> Result<Option<u8>, String> {
|
2026-05-25 04:53:51 +00:00
|
|
|
// Poll for a command byte with a nonblocking read. This avoids holding
|
|
|
|
|
// the stream lock across an awaited read while still removing the need
|
|
|
|
|
// for peek-based readiness checks.
|
2026-05-24 17:56:57 +00:00
|
|
|
let timeout_duration = Duration::from_millis(100);
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let stream = stream_locked.lock().await;
|
|
|
|
|
let mut buffer = [0; 1];
|
|
|
|
|
|
2026-05-25 04:53:51 +00:00
|
|
|
match stream.try_read(&mut buffer) {
|
|
|
|
|
Ok(1) => return Ok(Some(buffer[0])),
|
|
|
|
|
Ok(0) => {
|
|
|
|
|
drop(stream);
|
|
|
|
|
remove_stream_from_memory(stream_locked).await;
|
|
|
|
|
return Ok(None);
|
2026-05-24 17:56:57 +00:00
|
|
|
}
|
2026-05-25 04:53:51 +00:00
|
|
|
Ok(_) => {}
|
|
|
|
|
Err(err) if err.kind() == ErrorKind::WouldBlock => {
|
2026-05-24 17:56:57 +00:00
|
|
|
drop(stream);
|
|
|
|
|
sleep(timeout_duration).await;
|
|
|
|
|
}
|
2026-05-25 04:53:51 +00:00
|
|
|
Err(err) => {
|
|
|
|
|
warn!("Dropped stream: {err:?}");
|
|
|
|
|
drop(stream);
|
|
|
|
|
remove_stream_from_memory(stream_locked).await;
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
2026-05-24 17:56:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn peer_ip(stream_locked: &Arc<Mutex<TcpStream>>) -> String {
|
|
|
|
|
// Resolve the peer address once per incoming command so scoring and
|
|
|
|
|
// command handlers can use the same caller identity.
|
|
|
|
|
let stream = stream_locked.lock().await;
|
|
|
|
|
stream
|
|
|
|
|
.peer_addr()
|
|
|
|
|
.map(|addr| addr.ip().to_string())
|
|
|
|
|
.unwrap_or_else(|_| "unknown".into())
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 14:29:11 +00:00
|
|
|
fn skip_flood_check(command: u8, client_type: ClientType) -> bool {
|
|
|
|
|
if command == RPC_REPLY {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client_type == ClientType::Miner
|
|
|
|
|
&& matches!(
|
|
|
|
|
command,
|
|
|
|
|
RPC_BLOCK_HEIGHT | RPC_TORRENT_BY_HEIGHT | RPC_BLOCK_PIECE
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 17:56:57 +00:00
|
|
|
pub async fn next_incoming_command(
|
|
|
|
|
stream_locked: Arc<Mutex<TcpStream>>,
|
|
|
|
|
db: &Db,
|
|
|
|
|
connections_key: &str,
|
2026-06-01 14:29:11 +00:00
|
|
|
wallet: Arc<Wallet>,
|
2026-05-24 17:56:57 +00:00
|
|
|
) -> Result<Option<IncomingCommand>, String> {
|
|
|
|
|
// A disconnected socket returns None so the caller can end the RPC
|
|
|
|
|
// loop without treating a clean disconnect as a command failure.
|
2026-05-25 04:53:51 +00:00
|
|
|
let Some(command) = read_next_command_byte(&stream_locked).await? else {
|
2026-05-24 17:56:57 +00:00
|
|
|
return Ok(None);
|
2026-05-25 04:53:51 +00:00
|
|
|
};
|
2026-05-24 17:56:57 +00:00
|
|
|
let ip = peer_ip(&stream_locked).await;
|
|
|
|
|
|
|
|
|
|
// Connection memory is the source of truth for whether this stream
|
|
|
|
|
// belongs to a node/miner or a wallet-backed RPC client.
|
|
|
|
|
let client_type = get_client_type_from_memory(connections_key)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap_or(ClientType::Miner);
|
|
|
|
|
|
2026-06-01 14:29:11 +00:00
|
|
|
// Replies and miner sync traffic belong to expected node-to-node
|
|
|
|
|
// maintenance paths. All other commands still count against flood
|
|
|
|
|
// protection, including non-sync commands from miners.
|
|
|
|
|
if !skip_flood_check(command, client_type) {
|
|
|
|
|
check_request_frequency_with_client_type(db, ip.clone(), client_type, wallet).await;
|
2026-05-24 17:56:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Some(IncomingCommand {
|
|
|
|
|
command,
|
|
|
|
|
ip,
|
|
|
|
|
client_type,
|
|
|
|
|
}))
|
|
|
|
|
}
|