223 lines
6.7 KiB
Rust
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
|
|
}
|