223 lines
6.7 KiB
Rust

#![allow(clippy::doc_markdown)]
use std::{
net::{IpAddr, SocketAddr},
path::PathBuf,
sync::Arc,
};
use assets::Thumbnails;
use clap::Parser;
use lanspread_compat::eti::{self, EtiGame};
use lanspread_db::db::{Game, GameDB};
use lanspread_mdns::{
DaemonEvent,
MdnsAdvertiser,
LANSPREAD_INSTANCE_NAME,
LANSPREAD_SERVICE_TYPE,
};
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::Server as QuicServer;
use tokio::{io::AsyncWriteExt, sync::Mutex};
use tracing_subscriber::EnvFilter;
mod assets;
static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem"));
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
#[derive(Clone, Debug)]
struct ServerCtx {
handler: RequestHandler,
}
#[derive(Clone, Debug)]
struct ConnectionCtx {
server_ctx: Arc<ServerCtx>,
remote_addr: String,
}
async fn run(addr: SocketAddr, db: GameDB) -> eyre::Result<()> {
let mut server = QuicServer::builder()
.with_tls((CERT_PEM, KEY_PEM))?
.with_io(addr)?
.start()?;
let server_ctx = Arc::new(ServerCtx {
handler: RequestHandler::new(db),
});
while let Some(mut connection) = server.accept().await {
let conn_ctx = Arc::new(ConnectionCtx {
server_ctx: server_ctx.clone(),
remote_addr: maybe_addr!(connection.remote_addr()),
});
// spawn a new task for the connection
tokio::spawn(async move {
tracing::info!("{} connected", conn_ctx.remote_addr);
while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await {
let (mut rx, mut tx) = stream.split();
let conn_ctx = conn_ctx.clone();
// spawn a new task for the stream
tokio::spawn(async move {
tracing::trace!("{} stream opened", conn_ctx.remote_addr);
// handle streams
while let Ok(Some(data)) = rx.receive().await {
tracing::trace!(
"{} msg: (raw): {}",
conn_ctx.remote_addr,
String::from_utf8_lossy(&data)
);
let request = Request::decode(data);
tracing::debug!("{} msg: {:?}", conn_ctx.remote_addr, request);
let response = conn_ctx.server_ctx.handler.handle_request(request).await;
tracing::trace!("{} server response: {:?}", conn_ctx.remote_addr, response);
let raw_response = response.encode();
tracing::trace!(
"{} server response (raw): {}",
conn_ctx.remote_addr,
String::from_utf8_lossy(&raw_response)
);
// write response back to client
if let Err(e) = tx.write_all(&raw_response).await {
tracing::error!(?e);
}
// close the stream
if let Err(e) = tx.close().await {
tracing::error!(?e);
}
}
});
}
});
}
Ok(())
}
#[derive(Clone, Debug)]
struct RequestHandler {
db: Arc<Mutex<GameDB>>,
}
impl RequestHandler {
fn new(games: GameDB) -> RequestHandler {
RequestHandler {
db: Arc::new(Mutex::new(games)),
}
}
async fn handle_request(&self, request: Request) -> Response {
match request {
Request::Ping => Response::Pong,
Request::ListGames => {
let db = self.db.lock().await;
Response::Games(db.all_games().into_iter().cloned().collect())
}
Request::GetGame { id } => {
let db = self.db.lock().await;
match db.get_game_by_id(&id) {
Some(game) => Response::Game(game.clone()),
None => Response::GameNotFound(id),
}
}
Request::Invalid(data, err_msg) => {
tracing::error!(
"got invalid request from client (error: {}): {}",
err_msg,
String::from_utf8_lossy(&data)
);
Response::InvalidRequest(data, err_msg)
}
}
}
}
#[derive(Debug, Parser)]
struct Cli {
/// IP address to bind to.
#[clap(long)]
ip: IpAddr,
/// Listen port.
#[clap(long)]
port: u16,
/// Game database path (SQLite).
#[clap(long)]
db: PathBuf,
/// Games folder.
#[clap(long)]
folder: PathBuf,
/// Thumbnails folder.
#[clap(long)]
thumbnails: PathBuf,
}
fn eti_game_to_game(eti_game: EtiGame) -> Game {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
Game {
id: eti_game.game_id,
name: eti_game.game_title,
description: eti_game.game_readme_de,
release_year: eti_game.game_release,
publisher: eti_game.game_publisher,
max_players: eti_game.game_maxplayers,
version: eti_game.game_version,
genre: eti_game.genre_de,
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
thumbnail: None,
installed: false,
}
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let cli = Cli::parse();
let eti_games = eti::get_games(&cli.db).await?;
let mut games: Vec<Game> = eti_games.into_iter().map(eti_game_to_game).collect();
let thumbnails = Thumbnails::new(cli.thumbnails);
// add thumbnails to games
for game in &mut games {
if let Some(thumbnail) = thumbnails.get(&game.id) {
game.thumbnail = Some(thumbnail);
} else {
tracing::warn!("No thumbnail found: {}", game.id);
}
}
let game_db = GameDB::from(games);
let mdns = MdnsAdvertiser::new(
LANSPREAD_SERVICE_TYPE,
LANSPREAD_INSTANCE_NAME,
(cli.ip, cli.port).into(),
)?;
tokio::spawn(async move {
while let Ok(event) = mdns.monitor.recv() {
tracing::info!("mDNS: {:?}", &event);
if let DaemonEvent::Error(e) = event {
tracing::error!("mDNS: {e}");
break;
}
}
});
tracing::info!("Server listening on {}:{}", cli.ip, cli.port);
run(SocketAddr::from((cli.ip, cli.port)), game_db).await
}