This commit is contained in:
2025-11-08 20:56:35 +01:00
parent 50cd15867b
commit 82842c15c3
7 changed files with 820 additions and 0 deletions
+376
View File
@@ -0,0 +1,376 @@
#![allow(clippy::missing_errors_doc)]
use std::{fs::File, io::Write, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
use lanspread_db::db::{Game, GameFileDescription};
use lanspread_proto::{Message, Request, Response};
use s2n_quic::{
Client as QuicClient,
Connection,
Server,
client::Connect,
provider::limits::Limits,
stream::BidirectionalStream,
};
use tokio::{
io::AsyncWriteExt,
sync::{
RwLock,
mpsc::{UnboundedReceiver, UnboundedSender},
},
};
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem"));
#[derive(Debug)]
pub enum PeerEvent {
ListGames(Vec<Game>),
GotGameFiles {
id: String,
file_descriptions: Vec<GameFileDescription>,
},
DownloadGameFilesBegin {
id: String,
},
DownloadGameFilesFinished {
id: String,
},
DownloadGameFilesFailed {
id: String,
},
PeerConnected(SocketAddr),
PeerDisconnected(SocketAddr),
}
#[derive(Debug)]
pub enum PeerCommand {
ListGames,
GetGame(String),
DownloadGameFiles {
id: String,
file_descriptions: Vec<GameFileDescription>,
},
SetGameDir(String),
ConnectToPeer(SocketAddr),
}
async fn initial_peer_alive_check(conn: &mut Connection) -> bool {
let stream = match conn.open_bidirectional_stream().await {
Ok(stream) => stream,
Err(e) => {
log::error!("failed to open stream: {e}");
return false;
}
};
let (mut rx, mut tx) = stream.split();
// send ping
if let Err(e) = tx.send(Request::Ping.encode()).await {
log::error!("failed to send ping to peer: {e}");
return false;
}
let _ = tx.close().await;
// receive pong
if let Ok(Some(response)) = rx.receive().await {
let response = Response::decode(response);
match response {
Response::Pong => {
log::info!("peer is alive");
return true;
}
_ => {
log::error!("peer sent invalid response to ping: {response:?}");
}
}
}
false
}
async fn receive_game_file(
conn: &mut Connection,
desc: &GameFileDescription,
games_folder: &str,
) -> eyre::Result<()> {
log::info!("downloading: {desc:?}");
let stream = conn.open_bidirectional_stream().await?;
let (mut rx, mut tx) = stream.split();
let request = Request::GetGameFileData(desc.clone());
// request file
tx.write_all(&request.encode()).await?;
// create file
let path = PathBuf::from(&games_folder).join(&desc.relative_path);
let mut file = File::create(&path)?;
// receive file contents
while let Some(data) = rx.receive().await? {
file.write_all(&data)?;
}
log::debug!("file download complete: {}", path.display());
tx.close().await?;
Ok(())
}
async fn download_game_files(
game_id: &str,
game_file_descs: Vec<GameFileDescription>,
games_folder: String,
peer_addr: SocketAddr,
tx_notify_ui: UnboundedSender<PeerEvent>,
) -> eyre::Result<()> {
let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?;
let client = QuicClient::builder()
.with_tls(CERT_PEM)?
.with_io("0.0.0.0:0")?
.with_limits(limits)?
.start()?;
let conn = Connect::new(peer_addr).with_server_name("localhost");
let mut conn = client.connect(conn).await?;
conn.keep_alive(true)?;
let game_files = game_file_descs
.iter()
.filter(|desc| !desc.is_dir)
.filter(|desc| !desc.is_version_ini())
.collect::<Vec<_>>();
if game_files.is_empty() {
eyre::bail!("game_file_descs empty: no game files to download");
}
tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin {
id: game_id.to_string(),
})?;
// receive all game files
for file_desc in game_files {
receive_game_file(&mut conn, file_desc, &games_folder).await?;
}
let version_file_desc = game_file_descs
.iter()
.find(|desc| desc.is_version_ini())
.ok_or_else(|| eyre::eyre!("version.ini not found"))?;
// receive version.ini
receive_game_file(&mut conn, version_file_desc, &games_folder).await?;
log::info!("all files downloaded for game: {game_id}");
tx_notify_ui.send(PeerEvent::DownloadGameFilesFinished {
id: game_id.to_string(),
})?;
Ok(())
}
struct Ctx {
game_dir: Arc<RwLock<Option<String>>>,
}
#[derive(Clone, Debug)]
struct PeerCtx {
game_dir: Arc<RwLock<Option<String>>>,
}
pub async fn run_peer(
mut rx_control: UnboundedReceiver<PeerCommand>,
tx_notify_ui: UnboundedSender<PeerEvent>,
) -> eyre::Result<()> {
// peer context
let ctx = Ctx {
game_dir: Arc::new(RwLock::new(None)),
};
let peer_ctx = PeerCtx {
game_dir: ctx.game_dir.clone(),
};
// Start server component
let server_addr = "0.0.0.0:0".parse::<SocketAddr>()?;
let tx_notify_ui_clone = tx_notify_ui.clone();
let peer_ctx_clone = peer_ctx.clone();
tokio::spawn(async move {
if let Err(e) = run_server_component(server_addr, peer_ctx_clone, tx_notify_ui_clone).await
{
log::error!("Server component error: {e}");
}
});
// Handle client commands
loop {
let Some(cmd) = rx_control.recv().await else {
break;
};
match cmd {
PeerCommand::ListGames => {
// TODO: Implement peer discovery and game listing
log::info!("ListGames command received");
}
PeerCommand::GetGame(id) => {
log::info!("Requesting game from peer: {id}");
// TODO: Implement game fetching from peers
}
PeerCommand::DownloadGameFiles {
id,
file_descriptions: _,
} => {
log::info!("Got PeerCommand::DownloadGameFiles");
let games_folder = { ctx.game_dir.read().await.clone() };
if let Some(_games_folder) = games_folder {
// TODO: Implement peer file downloading
log::info!("Would download game files for {id}");
} else {
log::error!("Cannot handle game file descriptions: games_folder is not set");
}
}
PeerCommand::SetGameDir(game_dir) => {
*ctx.game_dir.write().await = Some(game_dir.clone());
log::info!("Game directory set to: {game_dir}");
}
PeerCommand::ConnectToPeer(peer_addr) => {
log::info!("Connecting to peer: {peer_addr}");
// TODO: Implement peer connection
}
}
}
Ok(())
}
async fn run_server_component(
addr: SocketAddr,
ctx: PeerCtx,
tx_notify_ui: UnboundedSender<PeerEvent>,
) -> eyre::Result<()> {
let limits = Limits::default()
.with_max_handshake_duration(Duration::from_secs(3))?
.with_max_idle_timeout(Duration::from_secs(3))?;
let mut server = Server::builder()
.with_tls((CERT_PEM, KEY_PEM))?
.with_io(addr)?
.with_limits(limits)?
.start()?;
let server_addr = server.local_addr()?;
log::info!("Peer server listening on {server_addr}");
// TODO: Implement mDNS advertising for peer discovery
while let Some(connection) = server.accept().await {
let ctx = ctx.clone();
let tx_notify_ui = tx_notify_ui.clone();
tokio::spawn(async move {
if let Err(e) = handle_peer_connection(connection, ctx, tx_notify_ui).await {
log::error!("Peer connection error: {}", e);
}
});
}
Ok(())
}
async fn handle_peer_connection(
mut connection: Connection,
ctx: PeerCtx,
tx_notify_ui: UnboundedSender<PeerEvent>,
) -> eyre::Result<()> {
let remote_addr = connection.remote_addr()?;
log::info!("{remote_addr} peer connected");
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerConnected(remote_addr)) {
log::error!("Failed to send PeerConnected event: {e}");
}
// handle streams
while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await {
let ctx = ctx.clone();
let remote_addr = Some(remote_addr);
tokio::spawn(async move {
if let Err(e) = handle_peer_stream(stream, ctx, remote_addr).await {
log::error!("{remote_addr:?} peer stream error: {e}");
}
});
}
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerDisconnected(remote_addr)) {
log::error!("Failed to send PeerDisconnected event: {e}");
}
Ok(())
}
async fn handle_peer_stream(
stream: BidirectionalStream,
ctx: PeerCtx,
remote_addr: Option<SocketAddr>,
) -> eyre::Result<()> {
let (mut rx, mut tx) = stream.split();
log::trace!("{remote_addr:?} peer stream opened");
// handle streams
loop {
match rx.receive().await {
Ok(Some(data)) => {
log::trace!(
"{remote_addr:?} msg: (raw): {}",
String::from_utf8_lossy(&data)
);
let request = Request::decode(data);
log::debug!("{remote_addr:?} msg: {request:?}");
match request {
Request::Ping => {
// Respond with pong
if let Err(e) = tx.send(Response::Pong.encode()).await {
log::error!("Failed to send pong: {e}");
}
}
Request::ListGames => {
// TODO: Return list of games from this peer
log::info!("Received ListGames request from peer");
}
Request::GetGame { id } => {
log::info!("Received GetGame request for {id} from peer");
// TODO: Handle game request
}
Request::GetGameFileData(desc) => {
log::info!(
"Received GetGameFileData request for {} from peer",
desc.relative_path
);
// TODO: Handle file data request
}
Request::Invalid(_, _) => {
log::error!("Received invalid request from peer");
}
}
}
Ok(None) => {
log::trace!("{remote_addr:?} peer stream closed");
break;
}
Err(e) => {
log::error!("{remote_addr:?} peer stream error: {e}");
break;
}
}
}
Ok(())
}